defmodule <%= inspect controller %> do
@moduledoc """
Example Phoenix controller demonstrating StarView with Datastar.
Features:
- Active search with debounced input
- Element patching with smart change detection
- Optimistic client-side filtering with data-show
"""
use <%= inspect web_module %>, :controller
@items [
"Elixir",
"Phoenix",
"LiveView",
"Datastar",
"SSE",
"Plug",
"Ecto",
"Ash",
"HEEx",
"Tailwind"
]
@impl StarView
def mount(conn, _params) do
conn
|> signal(:query, "")
|> signal(:results, @items)
end
@impl StarView
def render(assigns) do
~H"""
<div class="max-w-xl mx-auto p-6" data-signals={init_signals(@conn)}>
<h1 class="text-2xl font-bold mb-4">Active Search</h1>
<.search_form />
<.item_list results={@results} />
<.no_results query={@query} />
</div>
"""
end
def search_form(assigns) do
~H"""
<div class="mb-4 flex gap-2">
<input
type="text"
class="input grow"
placeholder="Search frameworks..."
data-bind:query
data-on:input__debounce.200ms={post("search")}
/>
<button class="btn" data-on:click={post("reset")}>
Reset
</button>
</div>
"""
end
attr :query, :string, default: nil
def no_results(assigns) do
~H"""
<div data-show={query_results("=== 0")}>
<p class="text-gray-500">
No results found for "<span data-text="$query">{@query}</span>"
</p>
</div>
"""
end
attr :results, :list, default: []
def item_list(assigns) do
~H"""
<ul id="item-list" class="grid gap-2" data-show={query_results("> 0")}>
<.item :for={item <- @results} item={item} />
</ul>
"""
end
attr :item, :string, required: true
def item(assigns) do
~H"""
<li class="border p-4" data-show={starts_with?("'#{@item}'")}>
{@item}
</li>
"""
end
@impl StarView
def handle_event("search", %{"query" => query} = signals, conn) do
conn
|> signal(:results, get_items(query))
|> maybe_patch_list(signals)
end
def handle_event("reset", signals, conn) do
conn
|> signal(:query, "")
|> signal(:results, @items)
|> maybe_patch_list(signals)
end
defp get_items(""), do: @items
defp get_items(query) do
search_query = String.trim(String.downcase(query))
Enum.filter(@items, &String.contains?(&1 |> String.downcase(), search_query))
end
# We use this to make the response as light as possible
# Compared to LiveView which does this automatically with change tracking
defp maybe_patch_list(%{assigns: %{results: x}} = conn, %{"results" => x}), do: conn
defp maybe_patch_list(conn, _signals), do: patch_element(conn, &item_list/1)
# JS helpers - for optimistic updates
defp starts_with?(item) do
"#{item}.trim().toLowerCase().startsWith($query.trim().toLowerCase())"
end
defp query_results(condition) do
"$results.filter(x => #{starts_with?("x")}).length #{condition}"
end
end