Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ability to pass LiveComponent as field #13

Merged
merged 1 commit into from
Mar 30, 2021
Merged

Conversation

edisonywh
Copy link
Owner

@edisonywh edisonywh commented Mar 30, 2021

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:

  • Your component has to be stateful, so you need to pass in :id
  • Your component has to use phx-keyup, because phx-change doesn't
    work outside of form.
  • Your component cannot have <form>, because HTML5 spec mandates that
    you cannot have nested form tags.

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
Copy link
Owner Author

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
Copy link
Owner Author

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?

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:
- Your component has to be stateful, so you need to pass in `:id`
- Your component has to use `phx-keyup`, because `phx-change` doesn't
work outside of `form`.
- Your component cannot have `<form>`, because HTML5 spec mandates that
you cannot have nested form tags.

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`.
@edisonywh edisonywh force-pushed the feature/live-component branch from f46fa5e to be8bd0a Compare March 30, 2021 16:23
@edisonywh edisonywh merged commit 2b25ed2 into main Mar 30, 2021
@edisonywh edisonywh deleted the feature/live-component branch March 30, 2021 16:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

LiveComponent support for rendering form fields
1 participant