Hi,
My background in components composition is mostly with DDAU in Ember.js. So it naturally serves me as a jump pad as I wrap my head around Live View and wonder how close my mental model is to the idiomatic LiveView approach.
Let me illustrate my thinking process with an example of breaking a modal component further down into smaller components.
I have a Dashboard
LiveView within which there is a UploadModal
that hosts PatientSelector
component.
PatientSelector
is a typical typeahead: the clinician types patient names and sees a list of patients whose names match his query.
The selected patient becomes a value of that typeahead and must be propagated to the parent UploadModal
component.
───────────────────────────────Chronological flow─────────────────────────────────▶
┌───────────────────────────────────────────────────────────────────────────────────┐
│ Upload Modal Live Component │
│ │
└─────────────────────────────────────────────▲────────────────────────────┬────────┘
│ │ │
│ │ │
destination │ │
for`on_selected` event sends `patient_selected` updates `selected_patient`
and `selected_patient` event to `on_selected` │
property target │
│ │ │
│ │ │
┌─────▼──────────────────────────────────────┴────────────────────────────▼────────┐
│ PatientSelector Live Component │
│ │
└──────────────────────────────────────────────────────────────────────────────────┘
▲
│
│
User selects patient
│
│
│
│
┌───────────────────┐
│ User │
└───────────────────┘
The cornerstone of this component is an input which triggers a patient lookup
<input
phx-debounce="100"
phx-focus={show_results()}
phx-blur={hide_results()}
name="search"
type="text"
autocomplete="off"
placeholder="Search by patient name or email"
value={@selected_patient.name}
/>
and then there is an autocompletion list in which items are entry points to patient selection:
<ul id="upload-modal-patient-selector-results" class="rounded">
<%= for patient <- @found_patients do %>
<li
phx-click="patient_selected"
phx-value-selected-patient-id={@patient.id}
phx-target={@myself}
>
As I understand it - I can’t assign a struct to phx-value-*
, so instead, I’m setting selected patient id phx-value-selected-patient-id={@patient.id}
.
Then, since PatientSelector
keeps a list of found_patients
in its state, I can pull Patient from that list in the event handler and send it to the parent component
def handle_event("patient_selected", %{"selected-patient-id" => selected_patient_id}, socket) do
selected_patient = Enum.find(socket.assigns.found_patients, &(&1.id == selected_patient_id))
send_update(socket.assigns.on_selected.module,
id: socket.assigns.on_selected.id,
selected_patient: selected_patient,
event: :patient_selected
)
{:noreply, socket}
end
Now, because selected_patient
is passed to the PatientSelector
, when I update it in the parent component, the child component also gets it.
<!-- somewhere in the parent component template -->
<.live_component module={PatientSelector} id="patient-selector-component" selected_patient={@selected_patient} on_selected={%{module: __MODULE__, id: @id}} current_user={@current_user} />
defmodule UploadModal do
use ObfuscatedWeb, :live_component
alias PatientSelector
def mount(socket) do
{:ok, assign(socket, :selected_patient, %{name: nil})}
end
def update(%{event: :patient_selected, selected_patient: selected_patient}, socket) do
{:ok, assign(socket, :selected_patient, selected_patient)}
end
def update(assigns, socket) do
{:ok, assign(socket, assigns)}
end
end
It all works fine, but I wonder if I’m going against the LV programming model with this or not. The primary motivation behind this wiring is to make the upload modal reusable without exposing its wiring/implementation details to hosting LiveView.
Pinging @chrismccord like he’s not busy enough already
UPDATE: I’m not sure how it happens but after patient being selected for the first time update
in the parent component is not triggered, but I still can see selected patient name in the parent component’s template.