How do you organize your components with Phoenix 1.7?

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.

1 Like

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

1 Like

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
1 Like

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.

7 Likes

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.

1 Like

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.

1 Like

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. :slight_smile: 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 :sweat_smile:

I work with old and new projects, both big and small, so I’ll be checking it out but with a grain of salt :wink: