Skip to main content
liveview-guidelines Phoenix LiveView patterns covering streams, JavaScript interop (hooks, colocated scripts, push events), testing, and form handling. Use when building or reviewing LiveView features.
설치로 이동 Skills Marketplace 커뮤니티가 만든 AI 스킬을 발견하고 탐색하세요.
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
직접 명령은 검토 Prompt를 거치지 않습니다. 실행하기 전에 소스를 확인하세요.
npx skills add https://github.com/code0100fun/botfiles --skill liveview-guidelines명령은 한 줄로 유지됩니다. 복사하기 전에 가로로 스크롤해 전체 내용을 확인하세요.
로컬 사본을 원하시나요? SkillsMP에서 현재 제공할 수 있는 파일을 다운로드하세요.
Zip 다운로드 다운로드 중... 관련 직업
SOC
이 저장소의 다른 Skills Guide for using bees, a local-first issue tracker (SQLite DB + a committed issues.jsonl) driven by the `bees` CLI. Use for ALL task/issue tracking on bees projects — creating issues, organizing them into epics with dependencies, the in_progress→closed lifecycle, and syncing/committing the tracker.
Guide for creating well-structured conventional commits. Use when creating git commits, drafting commit messages, or reviewing commit message quality.
My personal cross-codebase development workflow — set up tooling with mise, track work in Bees, make focused changes, commit with the commit-message-guide conventions, and land the plane cleanly. Use when starting or running a development task on a project that follows this workflow (mise + Bees + Git).
name liveview-guidelines description Phoenix LiveView patterns covering streams, JavaScript interop (hooks, colocated scripts, push events), testing, and form handling. Use when building or reviewing LiveView features.
Phoenix LiveView Guidelines
General
Never use the deprecated live_redirect and live_patch functions, instead always use <.link navigate={href}> and <.link patch={href}> in templates, and push_navigate and push_patch in LiveViews
Avoid LiveComponents unless you have a strong, specific need for them
LiveViews should be named like AppWeb.WeatherLive, with a Live suffix. When adding routes, the default :browser scope is already aliased with the AppWeb module, so you can just do live "/weather", WeatherLive
LiveView Streams
Always use LiveView streams for collections instead of assigning regular lists to avoid memory ballooning and runtime termination:
Basic append: stream(socket, :messages, [new_msg])
Reset stream: stream(socket, :messages, [new_msg], reset: true) (e.g. for filtering)
Prepend: stream(socket, :messages, [new_msg], at: -1)
Delete: stream_delete(socket, :messages, msg)
When using streams, the template must:
Set phx-update="stream" on the parent element with a DOM id
Consume @streams.stream_name and use the id as the DOM id for each child:
<div id="messages" phx-update="stream">
<div :for={{id, msg} <- @streams.messages} id={id}>
{msg.text}
</div>
</div>
LiveView streams are not enumerable. You cannot use Enum.filter/2 or Enum.reject/2 on them. To filter or refresh, refetch the data and re-stream with reset: true :
def handle_event("filter", %{"filter" => filter}, socket) do
messages = list_messages(filter)
{:noreply,
socket
|> assign(:messages_empty?, messages == [])
|> stream(:messages, messages, reset: true)}
end
Streams do not support counting or empty states . Track counts with a separate assign. For empty states, use Tailwind:
<div id="tasks" phx-update="stream">
<div class="hidden only:block">No tasks yet</div>
<div :for={{id, task} <- @streams.tasks} id={id}>
{task.name}
</div>
</div>
The empty state only works if it's the only HTML block alongside the stream for-comprehension.
When updating an assign that should change content inside streamed items, you MUST re-stream the items along with the updated assign:
def handle_event("edit_message", %{"message_id" => message_id}, socket) do
message = Chat.get_message!(message_id)
edit_form = to_form(Chat.change_message(message, %{content: message.content}))
{:noreply,
socket
|> stream_insert(:messages, message)
|> assign(:editing_message_id, String.to_integer(message_id))
|> assign(:edit_form, edit_form)}
end
Never use the deprecated phx-update="append" or phx-update="prepend" for collections
JavaScript Interop
When using phx-hook="MyHook" and that JS hook manages its own DOM, you must also set phx-update="ignore"
Always provide a unique DOM id alongside phx-hook
Inline Colocated JS Hooks Never write raw embedded <script> tags in HEEx. Instead, always use colocated js hook script tags :
<input type="text" name="user[phone_number]" id="user-phone-number" phx-hook=".PhoneNumber" />
<script :type={Phoenix.LiveView.ColocatedHook} name=".PhoneNumber">
export default {
mounted() {
this.el.addEventListener("input", e => {
let match = this.el.value.replace(/\D/g, "").match(/^(\d{3})(\d{3})(\d{4})$/)
if(match) {
this.el.value = `${match[1]}-${match[2]}-${match[3]}`
}
})
}
}
</script>
Colocated hooks are automatically integrated into the app.js bundle
Colocated hook names MUST ALWAYS start with a . prefix (e.g., .PhoneNumber)
External phx-hook External JS hooks must be placed in assets/js/ and passed to the LiveSocket constructor:
const MyHook = {
mounted ( ) { ... }
}
let liveSocket = new LiveSocket ("/live" , Socket , {
hooks : { MyHook }
});
Pushing Events Between Client and Server Use push_event/3 to push events/data to the client for a phx-hook to handle. Always return or rebind the socket:
socket = push_event(socket, "my_event", %{...})
# or return directly:
def handle_event("some_event", _, socket) do
{:noreply, push_event(socket, "my_event", %{...})}
end
Receive in JS hook with this.handleEvent:
mounted ( ) {
this .handleEvent ("my_event" , data => console .log ("from server:" , data));
}
Push from client to server with reply:
mounted ( ) {
this .el .addEventListener ("click" , e => {
this .pushEvent ("my_event" , { one : 1 }, reply => console .log ("got reply:" , reply));
})
}
Server handles with reply:
def handle_event("my_event", %{"one" => 1}, socket) do
{:reply, %{two: 2}, socket}
end
LiveView Tests
Use Phoenix.LiveViewTest module and LazyHTML (included) for assertions
Form tests are driven by render_submit/2 and render_change/2
Split test cases into small, isolated files. Start with simpler content-existence tests, then add interaction tests
Always reference key element IDs from your templates in tests for element/2, has_element/2, etc.
Never test against raw HTML; always use element/2, has_element/2, and similar
Focus on testing outcomes rather than implementation details
Be aware that Phoenix.Component functions may produce different HTML than expected. Test against output structure
When facing test failures with selectors, debug with LazyHTML:
html = render(view)
document = LazyHTML.from_fragment(html)
matches = LazyHTML.filter(document, "your-complex-selector")
IO.inspect(matches, label: "Matches")
Form Handling
Creating a Form from Params def handle_event("submitted", params, socket) do
{:noreply, assign(socket, form: to_form(params))}
end
When passing a map to to_form/1, it assumes string keys. You can specify a name:
def handle_event("submitted", %{"user" => user_params}, socket) do
{:noreply, assign(socket, form: to_form(user_params, as: :user))}
end
Creating a Form from Changesets %YourApp.Users.User{}
|> Ecto.Changeset.change()
|> to_form()
<.form for={@form} id="todo-form" phx-change="validate" phx-submit="save">
<.input field={@form[:field]} type="text" />
</.form>
Always give the form an explicit, unique DOM ID.
Avoiding Form Errors Always use a form assigned via to_form/2 in the LiveView, and the <.input> component in the template:
<%!-- ALWAYS do this (valid) --%>
<.form for={@form} id="my-form">
<.input field={@form[:field]} type="text" />
</.form>
<%!-- NEVER do this (invalid) --%>
<.form for={@changeset} id="my-form">
<.input field={@changeset[:field]} type="text" />
</.form>
You are FORBIDDEN from accessing the changeset in the template
Never use <.form let={f} ...>, always use <.form for={@form} ...> and drive all form references from the form assign