Patch lab

Each control sends a page event that uses a different Datastar patch mode. The target area makes insertion, replacement, and removal explicit.

Demo

Modes

Patch target

selector: #lab-target · mode: :outer · count: 0

Datastar patch-elements
Fresh stage, no patches yet. Next patch targets #lab-target.

Target node

#lab-target

Inner content starts here.

List children

#patch-list
Seed row
Sibling anchor for before and after #anchor-row
Remove mode deletes this note. #removable-note

How it works

  • outer replaces the target node.
  • inner replaces only target contents.
  • append and prepend add children to a list.
  • before and after insert siblings around an anchor.
  • remove deletes the selected element.
  • The selector is fixed per mode; the header and the ringed node show which one is next.

Every control posts a page event whose handler picks a different Datastar patch mode. mode: :outer swaps the target node, :inner only its children, :append and :prepend add list items, :before and :after insert siblings around an anchor, and remove_elements/2 deletes outright. The selector and the mode are the whole API.

lib/dstar_demo_web/pages/patch_lab_page.ex
"before" ->
  patch(conn, &patch_badge/1, [mode: mode, count: count],
    selector: "#anchor-row",
    mode: :before
  )

"after" ->
  patch(conn, &patch_badge/1, [mode: mode, count: count],
    selector: "#anchor-row",
    mode: :after
  )

"remove" ->
  remove_elements(conn, "#removable-note")

If this were LiveView

Only three of the seven modes have a LiveView counterpart, which is the point of the comparison rather than a gap in it.

Dstar

lib/dstar_demo_web/pages/patch_lab_page.ex
"before" ->
  patch(conn, &patch_badge/1, [mode: mode, count: count],
    selector: "#anchor-row",
    mode: :before
  )

"after" ->
  patch(conn, &patch_badge/1, [mode: mode, count: count],
    selector: "#anchor-row",
    mode: :after
  )

"remove" ->
  remove_elements(conn, "#removable-note")

LiveView

illustrative
# Illustrative LiveView equivalent — not compiled or run.
#
# LiveView has no patch-mode API at all: you change assigns and it works
# out the DOM operation. The nearest thing to append and prepend is a
# stream with :at; outer, inner, before and after have no equivalent
# because you never name the operation.
def mount(_params, _session, socket) do
  {:ok,
   socket
   |> assign(count: 0, note_visible?: true)
   |> stream(:rows, [%{id: "seed-row", label: "Seed row"}])}
end

def handle_event("append", _params, socket) do
  count = socket.assigns.count + 1

  {:noreply,
   socket
   |> assign(count: count)
   |> stream_insert(:rows, %{id: "row-" <> to_string(count), label: "append patch"}, at: -1)}
end

def handle_event("prepend", _params, socket) do
  count = socket.assigns.count + 1

  {:noreply,
   socket
   |> assign(count: count)
   |> stream_insert(:rows, %{id: "row-" <> to_string(count), label: "prepend patch"}, at: 0)}
end

def handle_event("remove", _params, socket) do
  {:noreply, assign(socket, note_visible?: false)}
end

def render(assigns) do
  ~H"""
  <div id="patch-list" phx-update="stream">
    <div :for={{dom_id, row} <- @streams.rows} id={dom_id}>{row.label}</div>
  </div>

  <div :if={@note_visible?} id="removable-note">Remove mode deletes this note.</div>
  """
end