Help me learn how to create a generic component

so i have this generic list of objects occuring in admin section of my app. I would like to make a generic component to get things easier and consistent looking app.

This can be a liveview component which takes in:
title in panel-heading
links for level_right div
a list of columns
a list of data to be displayed
action links (show edit delete)

my questions will be:

  • How to pass links to component ?
  • How to start thinking about breaking down components
  • should i look at surface components ? how different are they wrt phoenix components? will surface be merged into phoenix in near future ?

<article class="panel is-primary">

  <div class="panel-heading">

    <div class="level">

      {@Title}

      <div class="level-right">

        <span><%= live_patch "New Role", to: Routes.role_index_path(@socket, :new) , class: "button is-primary" %></span>

      </div>

    </div>

  </div>

  <a class="panel-block is-active">

  <table class="table is-striped is-hoverable is-fullwidth">

    <thead>

      <tr>

        <th> ID </th>

        <th>Name</th>

        <th> Created At </th>

        <th> Created By </th>

        <th> Active ? </th>

        <th>Actions</th>

      </tr>

    </thead>

    <tbody id="roles">

      <%= for role <- @roles do %>

        <tr id={"role-#{role.id}"}>

          <td><%= role.id %> </td>

          <td><%= role.name %></td>

          <td><%= role.inserted_at %></td>

          <td><%= role.creater_id %></td>

          <td><%= role.deleted_at %></td>

          <td>

            <span><%= live_redirect "Show", to: Routes.role_show_path(@socket, :show, role) %></span>

            <span><%= live_patch "Edit", to: Routes.role_index_path(@socket, :edit, role) %></span>

            <span><%= link "Delete", to: "#", phx_click: "delete", phx_value_id: role.id, data: [confirm: "Are you sure?"] %></span>

          </td>

        </tr>

      <% end %>

    </tbody>

  </table>

  </a>

</article>

bump. still wrapping head around surface

You might want to use slots from Surface, unless everything you’re passing in has the same structure.

I doubt Surface is being merged into Phoenix. Some things have been/will be migrated across or will be implemented in Phoenix in which case Surface can depend on that part of Phoenix and remove it’s implementation. E.g. html parser and maybe slots.

As to how to think about components, first, look at the parts of your code that are the same and turn that into a component. Read the docs on live view and surface on components.

well i stepped back and decided to use surface. turned out to be excellent choice. Thanks for your answer.

Here is how i did for reference.

A Panel component with two slots default and panel_heading_right

defmodule MyApp.Components.List do
  use Surface.Component

  prop panel_title, :string, default: "Panel"

  @doc "The color of the panel"
  # prop color, :string,
  #   default: "primary",
  #   values: ~w(white black light dark primary link info success warning danger)

  slot default
  slot panel_heading_right

  @doc "Css classes to propagate down to panel. Default class if no class supplied is simply _panel_"
  prop class, :css_class, default: []

  def render(assigns) do
    ~F"""
    <div class={["panel"] ++ @class}>
      <div class="panel-heading">
        <div class="level">
          {@panel_title}
          <div :if={slot_assigned?(:panel_heading_right)} class="level-right">
            <#slot name="panel_heading_right" />
          </div>
        </div>
      </div>
      <div class="panel-block is-active">
        <#slot> </#slot>
      </div>
    </div>
    """
  end
end

Then few more components Table and Table Column and finally use component as below

<MyApp.Components.List  panel_title="Roles" class="is-primary">
  <MyApp.Components.Table data={role <- @roles} expanded striped class="container">
    <MyApp.Components.Table.Column label="ID">
      {role.id}
    </MyApp.Components.Table.Column>
    <MyApp.Components.Table.Column label="Name">
      {role.name}
    </MyApp.Components.Table.Column>
    <MyApp.Components.Table.Column label="Actions">
        { live_redirect "Show", to: Routes.role_show_path(@socket, :show, role) }
        {live_patch "Edit", to: Routes.role_index_path(@socket, :edit, role) }
        {link "Delete", to: "#", phx_click: "delete", phx_value_id: role.id, data: [confirm: "Are you sure?"]}

    </MyApp.Components.Table.Column>

  </MyApp.Components.Table>
  <:panel_heading_right>
   { live_patch "New Role", to: Routes.role_index_path(@socket, :new) , class: "button is-primary"}
  </:panel_heading_right>

</MyApp.Components.List>
1 Like