edisonywh / backoffice

Admin tool built with the PETAL stack
MIT License
217 stars 15 forks source link

Ability to pass LiveComponent as field #13

Closed edisonywh closed 3 years ago

edisonywh commented 3 years ago

This commit enables user to pass in custom LiveComponent for more complex form field.

User do it like so:

form do
    field :newsletter_id, :component, render: SuggestionComponent, id: :newsletter_suggestion
end

This creates a stateful LiveComponent that can update your form input values.

This works with a few assumption:

In your Suggestion component, you can show user the available suggestions, then when they click, you should handle the event and then let the FormComponent know. You do this by:

send self(), {:pick, {:your_field_name, value: value}}

This will forward the request to the parent LiveView, which will then broadcast it to the FormComponent. For example, say you are editing a schema called Listing. It has a field called newsletter_id (belongs_to), and you want to have suggestions when user is typing so they can search in-line. You would then send it as:

send self(), {:pick, {:newsletter_id, value: 4}}

This would create a hidden_input on your <form>, which would update :newsletter_id to be 4.

Fixes #2

edisonywh commented 3 years ago

This is what an example Suggestion component will look like:

defmodule SlickWeb.NewsletterSuggestionComponent do
    use SlickWeb, :live_component

    def mount(socket) do
      socket =
        socket
        |> assign(:suggestions, [])
        |> assign(:picked, nil)

      {:ok, socket}
    end

    def render(assigns) do
      ~L"""
      <input type="text" value="<%= @picked || @value %>" phx-debounce="500" phx-target="<%= @myself %>" phx-keyup="suggest" class="mt-1 focus:ring-indigo-500 focus:border-indigo-500 block w-full shadow-sm sm:text-sm border-gray-300 rounded-md transition">
      <%= if @suggestions != [] do %>
        <div class="relative">
          <div class="absolute left-0 right-0 rounded-md border border-gray-50 shadow-lg py-2 bg-white">
            <%= for {id, name} <- @suggestions do %>
              <div
                phx-target="<%= @myself %>"
                phx-click="pick"
                phx-value-id="<%= id %>"
                phx-value-name ="<%= name %>"
                class="cursor-pointer p-2 hover:bg-gray-200 focus:bg-gray-200">
                <%= name %>
              </div>
            <% end %>
          </div>
        </div>
      <% end %>

      """
    end

    def handle_event("suggest", %{"value" => ""}, socket) do
      socket = socket |> assign(:suggestions, [])

      send self(), {:pick, {:newsletter_id, value: nil}}

      {:noreply, socket}
    end

    def handle_event("suggest", %{"value" => value}, socket) do
      require Ecto.Query

      suggestions =
        Slick.Newsletters.list_newsletters(query: value)
        |> Ecto.Query.limit(4)
        |> Ecto.Query.select([:id, :name])
        |> Slick.Repo.all()
        |> Enum.map(&({&1.id, &1.name}))

      socket = socket |> assign(:suggestions, suggestions)

      {:noreply, socket}
    end

    def handle_event("pick", %{"id" => id, "name" => name}, socket) do
      socket =
        socket
        |> assign(:picked, name)
        |> assign(:suggestions, [])

      send self(), {:pick, {:newsletter_id, value: id}}

      {:noreply, socket}
    end
  end
edisonywh commented 3 years ago

This works right now, but I'm not sure if this is a good use case to allow users to add custom hidden_input to form. Thoughts?