Good practice for UI function component

evening elixir members!

I am using tailwindcss and phoenix together. And I like to create function components for certain UI components such as Card, Dropdown and etc.

Why?

because I prefer easy update of component style. And easy, <.card>card content blabla</.card> simple easy powerful nice!

Example of Function Component

# This is an example of component usage in LiveView.
        <.dropdown alignment={:end}>
          <%# Use atom :end or :start to control the dropdown menu alignment. %>
          <:button color={:blue}>Click here</:button>
          <%# The use of atom :blue to control the button color. %>
          <:item>Dropdown Item 1</:item>
          <:item>Dropdown Item 2</:item>
        </.dropdown>

# Here is the FunctionComponent Code
  def dropdown(assigns) do
    assigns =
      assigns
      |> assign_new(:alignment, fn -> "" end)
      |> assign_new(:direction, fn -> "" end)
      |> assign_new(:open, fn -> "" end)
      |> assign_new(:class, fn -> "" end)
      |> assign_new(:item, fn -> [] end)

    ~H"""
    <div
      class={"dropdown#{direction(@direction)}#{alignment(@alignment)}#{open(@open)}"}
      id={"#{@id}-dropdown"}
    >
      <%= for button <- @button do %>
        <label
          tabindex="0"
          class={
            "#{button_class(button)}#{button_color(button)}#{button_outline(button)}#{button_size(button)}"
          }
        >
          <%= render_slot(button) %>
        </label>
      <% end %>
      
      <ul tabindex="0" class="dropdown-content menu p-2 w-52">
        <%= for {item, i} <- Enum.with_index(@item), item_id="#{@id}-dropdown-item-#{i}" do %>
          <li id={item_id}><%= render_slot(item) %></li>
        <% end %>
      </ul>
    </div>
    """
  end

  # Ignore the class name, I simplify them for better reading here.
  defp direction(:top), do: " dropdown-top"
  defp direction(:left), do: " dropdown-left"
  defp direction(_), do: ""

  defp alignment(:end), do: " dropdown-end"
  defp alignment(_), do: ""

  defp open(true), do: " dropdown-open"
  defp open(_), do: ""

  # I use patterm matching to match the params
  defp button_class(%{class: class}), do: class
  defp button_class(_), do: ""

  defp button_color(%{color: :blue}), do: " btn btn-blue m-1"
  defp button_color(%{color: :gray}), do: " btn btn-gray m-1"
  defp button_color(_), do: " btn m-1"

  defp button_outline(%{outline: true}), do: " btn-outline"
  defp button_outline(_), do: ""

  defp button_size(%{size: :large}), do: " btn-lg"
  defp button_size(%{size: :small}), do: " btn-sm"
  defp button_size(_), do: ""

My Question Is

is it a good practice to use Atom to control the component like color={:blue} to control the button color?
I am not sure of FunctionComponent like this is a good idea, would like to hear your opinion.
There will be more params like <.dropdown alignment={:end} direction={:top} open>

One more thing i am thinking is how to make the color={:blue} works better, since no body know the atom is named :blue until we have to check the FunctionComponent to see the available options. Is there a better method for this? like raising error if we set :blueblue which is not exists

1 Like

Standard simple UI components like cards and dropdowns are usually available in component libraries. Have you checked them out? For example this one:

(Don’t get misguided by the name, AlpineJS is not required)

NICE! Currently I prefer to create my own UI components instead of using libraries. reasons are more control of the component and learning opportunities.

1 Like

If you’re doing this to learn, that’s of course perfectly fine :+1:

However, in my experience, the “more control” argument in a real project is 90% of the times an illusion where developers trade a tiny amount of “control” for a lot more bugs, technical debt, and code to maintain and understand.

The NIH syndrome is an endemic disease in our industry :grimacing:

1 Like

@trisolaran You want to be confident that this UI library you’re using is going to be maintained, is easy to add your own style too, is customisable but doesn’t have too much bloat, etc

Quicker to fix the bugs in your project than someone else’s though :wink:. For example, those dropdowns you linked don’t even display correctly on mobile.

3 Likes

@cmo fair enough! My intent wasn’t to say: “use this library, it’s great”. What I wanted to say is: “check this and other libraries out before you reinvent the wheel”

I agree these are things you want to check before using a library, but @loon wasn’t even aware that the library existed apparently :wink:

Yeah, as long as the people who wrote the code are still in the team, or you still remember what you were thinking when writing your code. Try to fix the bugs of some custom, undocumented code written by team members who left the company years ago. You’ll be scratching your head wondering “what did they want to do?” and probably also asking yourself “why didn’t they use library X?”

On the other hand, if a library is popular and has traction, its bugs are everyone’s problem. My experience with the open source community is that teams that maintain popular packages are generally very receptive and quick at fixing problems.

I’m not saying that using a library is always the way to go. As you said, you need to check that it satisfies a minimum set of requirements. But it’s often the way to go, especially if you’re dealing with common problems that have been solved to death

2 Likes

Actually @cmo 's point is nothing wrong and @trisolaran you are right as well. I think this is a personal pattern.

My personal thinking is that:

Since i am using tailwindcss (a framework) and phoenix (a framework), and phoenix provide such an awesome FunctionComponent already. Why don’t I utilise the FunctionComponent as much as possible instead of adopting another framework (like Petal.build). Nothing wrong with Petal.build, I just prefer to create new one :slight_smile:

another reasons from me are:

  • Highly customizable of component (you create the component, you know exactly what you are doing)
  • less worry about framework breaking Changes (I try to avoid too many frameworks on top of another frameworks) where I will feel outdated if i don’t update the package.
  • Less worry about the library is still maintained. Bug with my component? blame myself, no blame game to play.
  • With tailwindcss, it is not hard to craft a beautiful component anyway. I personally love to spend the extra time to play with the styling and crafting the component. For really urgent project, I use framework like Bootstrap, DaisyUI and the mentioned Petal.build (Would love to)

By the way,

Let’s get back to the question.

2 Likes

I tend to use strings, but I’m not sure if it really matters that you’re using atoms.

You can specify the possible prop/attr values with values or values! in Surface.

@doc "The color of the button"
prop color, :string, values: ["green", "yellow", "gray"], default: "gray"

...
# looks more like HTML
<Button type="submit" color="green">Save</Button>

ahh! good idea! I went to read the source code of Petal.build, they do similar things.

But I realize they are using options like

# prop color, :string, options: ["primary", "secondary", "info", "success", "warning", "danger", "gray"]

what’s the different between values and options in the prop?

If you don’t want to use a big framework but don’t mind using a thin library, you can have a look at this one (I’m the author)

We wrote 50+ components with it.
It will be updated to support LiveView declarative assigns when LV 0.18.0 is released

2 Likes

I believe their props have nothing to do with Surface’s props (they’re not using Surface). Those # prop ... comments seem to be just a way to document the assigns accepted by the component and the possible values for each assign (which they call options).

1 Like

This library looks nice. That extend_class function seems very handy! :+1:

1 Like

Ahh!! The prop and values gave me big question mark. Google it and couldn’t find a thing. Thanks!

@cblavier nice!!! thanks

1 Like

This Topic/Question seem like opinion based. I will mark few important points here for your reference.

  1. You can use Petal.build lib for your UI components.

    • You can refer to their source for more ideas if you really want to make your own component.
  2. Another way (if you use surface)

  3. And a thin library

I couldn’t pick a :white_check_mark: Solution here, because many things mentioned here are informative and good. So please check above answers.