It’s pretty much to avoid mentioning my “WhateverWeb” module name and making it universal for any user to simply copy-paste into their module between do and end. It’s taking a current module name, splitting it so that you can get a parent module name and then concatenating it with a CoreComponents atom.
How do you add it to html_helpers/0 ? I am trying to make it work by adding it as:
use MyAppWeb.Components
but then I get a
== Compilation error in file lib/my_app_web/controllers/page_html.ex ==
** (CompileError) lib/my_app_web/controllers/page_html.ex:2: module MyAppWeb.Components.CustomComponents.MyButton is not loaded and could not be found. This may be happening because the module you are trying to load directly or indirectly depends on the current module
expanding macro: MyAppWeb.Components.__using__/1
Code for further reference:
#components.ex
defmodule MyAppWeb.Components do
defmacro __using__(_) do
quote do
import unquote(__MODULE__).CustomComponents.{
MyButton
# ....
}
end
end
end
And the directory looks like this:
my_app_web/
|-- components/
| |-- custom_components/
| | |-- my_button.ex
| | |-- other_component1.ex
| |-- components.ex
We would need to see the code in my_button.ex to know for sure but it looks like you just don’t have the MyButon module properly defined. It must be:
defmodule MyAppWeb.Components.CustomComponents.MyButton do
# ...
end
Saw this thread pop up and thought I’d share how I’ve ended up doing this.
I mostly work with umbrella applications, where we usually have a separate application which handles assets and components. Depending on the project, it’s been called either DesignSystem, WebAssets, CommonWeb or similar. In the most recent one it’s called DesignSystem so I’ll go with that one for the examples. All the components are in separate files and the folder structure is something along the lines of:
apps/
|-- design_system/
||-- assets/
|||-- css/
|||-- js/
||-- lib/
|||-- design_system/
||||-- components/
|||||-- form/
||||||-- error.ex
||||||-- input.ex
|||||-- button.ex
|||||-- table.ex
|||-- design_system.ex
...
Component’s module naming follows the folder structure.
defmodule DesignSystem.Components.Button do
def button(assigns) do
...
end
end
In design_system.ex I have a defdelegate for every component, which allows me to call them without specifying the full module name.
defmodule DesignSystem do
defdelegate button(), to: __MODULE__.Button
defdelegate input(), to: __MODULE__.Form.Input
...
end
Verbose code is more to my liking, and it also allows easy code completion, so I don’t mind my .heex files looking like this:
<DesignSystem.box>
<DesignSystem.button>
Click me!
</DesignSystem.button>
</DesignSystem.box>
To deal with typing out the DesignSystem part every time, I would either import or alias the module:
defmodule ExampleWeb do
def live_view() do
quote do
use Phoenix.LiveView,
layout: {ExampleWeb.Layouts, :live}
# Either this
import DesignSystem
# Or this
alias DesignSystem, as: DS
unquote(html_helpers())
end
end
end
The reason I don’t do this is because in my view it’s easier for beginners to clearly see where the function is defined at. Also this one time I made a component called Form and I wanted to call it with <.form> but it’s already defined in Phoenix.Component, and I didn’t want to call it .simple_form or similar. So yeah, a ridiculous reason but I am a ridiculous person.
Hi. Would like to ask, do you have any public repository where this approach is implemented?
Hi!
I didn’t have one but I made a simple example for you to look at.
I try to avoid having gettext calls inside UI components and instead pass the strings from the caller’s side. This is where slots come in handy. You’ll see this in how I’ve implemented the Flash and FlashGroup components, found in eggsample_web/lib/eggsample_web/components/layouts/app.html.heex#3. This reminds me that I should really look into Live Toast…
Basically this example has CoreComponents pulled into a separate app, with a workaround for the existing translate_error function used in inputs. It relies on implementing the error_translation_function somewhere and configuring it for the UI app to use.
You’ll find the configuration in config/config.exs#71 and it’s implementation in eggsample/lib/eggsample/gettext.ex#4. It’s used in ui/lib/ui.ex#18, with an example call in ui/lib/ui/components/form/input.ex#64.
Some basic examples exist in eggsample_web/live/home/index.ex, which is the page you’ll see when visiting the root route.
The solution is not perfect (this example is not exactly the same as we have in our codebase) but it has served us quite well.
Whooah… man! I din’t even expected such generosity! Thank You for your TIME! I will explore it now. Right away i see couple of interesting things - for example, separated UI layer which is also something I am looking into.
Looks like you have the same “issue” I were trying to solve. For example, UI is an dependency of Web. When you are developing (watching) Web, you might want to tweak some UI components and “it would be nice to see hot reload there as well”. But it doesn’t work that way.
You need to restart the “phx.server”. I had worked with Bazel build system before, which basically is multi-repo system, which “feels” (from DX perspective) like a monorepo. You tweak any repo/part of your solution and Bazel takes care about diffing and compiling only those parts which are affected by the changes. Builds were like 1-2 second long for any kind of change.
Yeah you’re right. We’ve been moving away from umbrella apps since we haven’t seen the value in it for our projects. For regular non-umbrella projects, Boundary is nice for enforcing… well, boundaries. Actually pretty much anything by Saša Jurić has worked very well for us.
Bazel looks nice, I’ll have to check it out, thanks!
Oh no!.. Don’t walk that road. Bazel is HUGE rabbit hole! It is worth investing into it for larger shops and from very beginning. It is hard to jump into it for established projects as everything should be done “Bazel way”. And I’m not sure is there Elixir rules to work with it.
Haha ok, thanks for the warning ![]()
I work with old and new projects, both big and small, so I’ll be checking it out but with a grain of salt ![]()






















