How do you organize your components with Phoenix 1.7?

Are there any downsides, like perf issues, to putting all functional components in CoreComponents as long as you prefix it with the context for organization, i.e. blog_navbar, app_navbar ?
Previously I’ve sectioned them out by context into their own modules, but I think this may not have many benefits as I often prefix the function with its context.

What’s your approach?

I don’t like the CoreComponents module approach. To me it feels like that messy drawer in your house where you keep that extra battery, plastic sunglasses and all sort of random things.

What I like to do, is create more topical modules such as Layout, Input or Modal. Then you can alias them where you’d import CoreComponents and then call from anywhere <Layout.sidebar />, <Input.select />, <Modal.toast /> etc etc.

Maybe at first the benefits aren’t that great, but consider these case:

<Layout.sidebar /> v/s <.sidebar_layout />

How do you know where sidebar_layout is coming from? You’d need to check for an implementation in the current module, inside any of the imports, inside your use call or any of it’s imports. That can get really tricky as the project grows. Layout.sidebar stays O(1) for definition retrieval.

Another complexity CoreComponent introduces is the question to the developer: Should this component be a CoreComponent? That is not always easy to answer and ultimately meaningless. It’s the worst kind of decision to make.

6 Likes

I break them up by type under a Components namespace and just import everything everywhere.

While one of my favourite things about Elixir is the locality—I very, very rarely alias and only import if there is one per module so it’s obvious where it’s coming from—this isn’t important to me at all when it comes to components because it’s dead obvious that <.foo /> is a component so I know where to look/grep. The additional module provides no further context, I just grep in live/my_app_web/components/. You also learn very quickly what you own and what comes from Phoenix.

I move stuff from CoreComponents into MyAppWeb.Components.Form, MyAppWeb.Components.Modal, etc and create others. Then my Components module looks like:

defmodule MyAppWeb.Components do
  defmacro __using__(_) do
    import unquote(__MODULE__).{
      Form,
      Modal,
      Links,
      # ....
    }
  end
end

Then use MyAppWeb.Components in live_view/0 in lib/my_app_web.ex and bam, all my components are available everywhere.

The drawback here is that you can’t use MyAppWeb, :component in your components as you’ll obviously get circular dependencies and it won’t compile. If I do need to reference other components in my components, I just use the fully qualified version, but of course you can also alias or whatever.

3 Likes

So are there actually perf improvements by using Module.component or by definition retrieval you meant navigating the codebase?

In either case I understand the appeal, especially for large Phoenix projects.
What I’m proposing in the OP would be a nightmare on a team with more than a couple devs.
Interesting way to go about it, thanks!

Sectioning them out but still importing them all seems interesting. I think the main appeal to one large CoreComponents is an ability to reference every single component in another one: can that be achieved here? I.e. you can use all the Links components in components defined within the Form or Modal

Yes but you have to full qualify them. The big drawback ehere is that it’s easier to introduce cyclical references. This can be avoided if you sort of think about your components in “layers” (there’s a correlation to Tailwind’s layers there) or actually they could be metaprogrammed into one big module. Or they could just be one big module, of course. I’ve learned to accept larger modules since getting comfortable with FP but the way I navigate codebases is still suited to smaller files—for example, if I’m working on a new file with no component calls yet and I want to use a form component, my first instinct is to fuzzy search for comp/form then scroll through the file to see what I got, so that’s why I break them up like that. I could always improve my editor tooling, of course.

CoreComponents is 100% my junk drawer and I’m ok with it… :slight_smile:

That’s not to say we don’t have ClientComponents, AcaComponents, WhateverFeatureComponents

But I don’t think having a junk drawer is bad until it overflows and then you start organizing.

I’ve been trying to figure this out too and have come up with the following guidelines for my own projects. Anyone see anything I’m not thinking of here?

  • Core Components – aka components that the generators rely on – live within components/core_components.ex
    - These are imported into view_helpers in the whatever_web.ex file so are accessible through any view, live component, or component by default
    - Aside: My main concern with customizing core_components.ex is having there be a major update that it’s then tedious to update the styles per component. I also think for this reason that this file should only have the functions it ships with within it.
  • Other global components, such typography or additional form elements, should be set up as modules in /components/ directory and imported into view_helpers
  • ALL other non-live components should live within /components/ too, but if they’re specific to certain views, they should only be imported as needed
  • Live components, such as a form component that you’re using in one specific liveview, should be colocated in the same directory as the corresponding liveview and template files and imported only into files as needed

My 2 cents (feeling adventures)

flash_components.ex:

defmodule ProjectWeb.FlashComponents do
  @parent __MODULE__ |> Module.split() |> Enum.drop(-1) |> Module.concat()
  @core Module.concat(@parent, CoreComponents)

  defdelegate flash(assigns), to: @core
  defdelegate flash_group(assigns), to: @core
end

form_components.ex:

defmodule ProjectWeb.FormComponents do
  @parent __MODULE__ |> Module.split() |> Enum.drop(-1) |> Module.concat()
  @core Module.concat(@parent, CoreComponents)

  defdelegate simple_form(assigns), to: @core
  defdelegate button(assigns), to: @core
  defdelegate input(assigns), to: @core
  defdelegate label(assigns), to: @core
end

modal_components.ex:

defmodule ProjectWeb.ModalComponents do
  @parent __MODULE__ |> Module.split() |> Enum.drop(-1) |> Module.concat()
  @core Module.concat(@parent, CoreComponents)

  defdelegate modal(assigns), to: @core

  alias Phoenix.LiveView.JS
  defdelegate show_modal(js \\ %JS{}, id), to: @core
  defdelegate hide_modal(js \\ %JS{}, id), to: @core
end

show_components.ex:

defmodule ProjectWeb.ShowComponents do
  @parent __MODULE__ |> Module.split() |> Enum.drop(-1) |> Module.concat()
  @core Module.concat(@parent, CoreComponents)

  defdelegate header(assigns), to: @core
  defdelegate table(assigns), to: @core
  defdelegate list(assigns), to: @core
  defdelegate back(assigns), to: @core

  alias Phoenix.LiveView.JS
  defdelegate show(js \\ %JS{}, selector), to: @core
  defdelegate hide(js \\ %JS{}, selector), to: @core
end

error_components.ex:

defmodule ProjectWeb.ErrorComponents do
  @parent __MODULE__ |> Module.split() |> Enum.drop(-1) |> Module.concat()
  @core Module.concat(@parent, CoreComponents)

  defdelegate error(assigns), to: @core

  defdelegate translate_error(params_tuple), to: @core
  defdelegate translate_errors(errors, field), to: @core
end

And certainly project_web.ex will have to rewire:

  defp html_helpers do
    quote do
      # ...
      # import ProjectWeb.CoreComponents
      import ProjectWeb.FormComponents
      import ProjectWeb.FlashComponents
      import ProjectWeb.ModalComponents
      import ProjectWeb.ShowComponents
      import ProjectWeb.ErrorComponents
3 Likes

There are a couple of tools that will show you the diff between versions.

This file is full of examples to be modified by the user. We don’t want another bootstrap situation where all the liveview sites look the same do we :upside_down_face:?

1 Like

Tis is how I do it too.

2 Likes

So succinct, loving it!