用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
直接命令不会经过审查 Prompt;运行前请先检查来源。
npx skills add https://github.com/dvcrn/skills --skill phoenix-colocated-hooks命令会保持在同一行。复制前请横向滚动并检查完整内容。
想先保存到本地?可下载 SkillsMP 当前能够提供的文件。
Install and configure PostHog in an Elixir/Phoenix project, including backend SDK, runtime configuration, an app-owned analytics boundary, identity wiring, JS client assets, template wiring, Plug middleware, and verification. Use when adding PostHog analytics or event capture to an Elixir or Phoenix application.
Install and configure Sentry in an Elixir or Phoenix project using official modern defaults, including runtime DSN configuration, Sentry.LoggerHandler with log message capture and rate limiting, Sentry.PlugContext placement, Cowboy/Bandit server detection, Oban error reporting, PII scrubbing, source code packaging for releases, and verification. Use when adding Sentry error monitoring or updating Sentry setup in an Elixir codebase.
SwiftUI state management using @Observable Store containers (Observation framework, iOS 17+), @Environment injection, store composition, derived state, and async mutation patterns. Use when designing or reviewing Store architecture, migrating from ObservableObject/Combine, translating React hooks or React Query patterns to SwiftUI, or implementing shared app-wide state containers.
正在显示 SKILL.md
| name | phoenix-colocated-hooks |
| description | Use when we need an explanation of phoenix colocated hooks |
Colocated hooks let you define Phoenix LiveView client hooks directly alongside your HEEx templates. They live inside a <script> block with a special :type and are compiled into a JS manifest that bundlers can import.
.heex/~H templateDefine a hook inside your LiveView template using the Phoenix.LiveView.ColocatedHook type, and reference it with phx-hook on an element:
defmodule MyAppWeb.DemoLive do
use MyAppWeb, :live_view
def mount(_params, _session, socket) do
{:ok, socket}
end
def render(assigns) do
~H"""
<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>
"""
end
end
Key points:
<script> tag uses :type={Phoenix.LiveView.ColocatedHook}name attribute is required and must start with a dot (e.g. .PhoneNumber, not PhoneNumber)phx-hook attribute value must also include the dot (e.g. phx-hook=".PhoneNumber", not phx-hook="PhoneNumber")MyAppWeb.DemoLive.PhoneNumber)At compile time, Phoenix extracts colocated hooks and writes them into a generated JS folder (typically under _build). A manifest file aggregates hooks as named exports for your bundler:
import { hooks } from "phoenix-colocated/my_app"
console.log(hooks)
/*
{
"MyAppWeb.DemoLive.PhoneNumber": { ... },
...
}
*/
Important details:
mix compile before running your assets pipeline so hooks existcompile runs before assets.deploy:# Instead of
release: ["assets.deploy", "release"]
# Use
release: ["compile", "assets.deploy", "release"]
Runtime hooks are colocated hooks that are not extracted into the JS manifest. They are executed directly in the browser, which is useful when you cannot change the main JS bundle (for example, when extending Phoenix.LiveDashboard).
To register a runtime hook, add the runtime attribute and make the script body evaluate to the hook object (no export default):
<script :type={Phoenix.LiveView.ColocatedHook} name=".MyHook" runtime>
{
mounted() {
// your hook logic here
}
}
</script>
LiveView wraps this content into a function on window:
window["phx_hook_HASH"] = function () {
return {
mounted() {
// ...
}
}
}
Notes and caveats:
name convention and get module-prefixedIf you use runtime hooks in an app with Content Security Policy (CSP), inline scripts must be allowed, usually via a nonce.
Example with nonce:
<script
:type={Phoenix.LiveView.ColocatedHook}
name=".MyHook"
runtime
nonce={@script_csp_nonce}
>
function () {
return {
mounted() {
// logic
}
}
}
</script>
The nonce in @script_csp_nonce must match the one advertised in your Content-Security-Policy response header.
Use this skill when you need:
When not to use colocated hooks:
phoenix-hooks skill).phx-hook semantics (no LiveView hook lifecycle, just exports/utilities), use Phoenix.LiveView.ColocatedJS instead (see the phoenix-colocated-js skill).If Alpine.js is already installed in your project, use it for local UI state and use LiveView hooks for coordinating with the server. If Alpine.js is not installed and you need rich client-side state, ask the team whether you should add Alpine.js before implementing these patterns.
Use Alpine for local UI state and LiveView hooks for server coordination. Prefer Phoenix.LiveView.JS for simple UI transitions; reach for hooks only when you need server feedback to update client state.
<%!-- Define as a ColocatedHook; note dotted name --%>
<script :type={Phoenix.LiveView.ColocatedHook} name=".SaveHook">
export default {
mounted() {
this.handleEvent("save_complete", ({ success, error }) => {
const alpine = this.el._x_dataStack?.[0];
if (!alpine) return;
alpine.loading = false;
if (!success) alpine.error = error;
})
}
}
// Elements using this hook must have an id
// Use with: phx-hook=".SaveHook" (include the dot)
</script>
<div id="save-form" phx-hook=".SaveHook" x-data="{
loading: false,
error: '',
save() {
this.loading = true;
this.error = '';
this.$refs.saveButton.click();
}
}">
<input x-on:keydown.cmd.s.prevent="save()" />
<span x-show="error" x-text="error"></span>
<button x-on:click="save()" x-bind:disabled="loading">
<span x-show="!loading">Save</span>
<span x-show="loading">Saving...</span>
</button>
<button x-ref="saveButton" phx-click="save" class="hidden"></button>
</div>
Server handler example:
def handle_event("save", params, socket) do
case save_operation(params) do
{:ok, _} ->
{:noreply, push_navigate(socket, to: "/success")}
{:error, reason} ->
socket = push_event(socket, "save_complete", %{success: false, error: reason})
{:noreply, socket}
end
end
.MyHook).phx-hook too; names must match exactly.id.this.el._x_dataStack?.[0].x-show, ensure the hidden state sets style="display: none;" initially to avoid FOUC.x-on:event syntax, not @event.Jason.encode!.Prefer Phoenix.LiveView.JS for fast, declarative UI changes.
<%=
JS.show(to: "#dialog-id", display: "flex")
|> JS.add_class("backdrop-fade-in", to: "#dialog-id-backdrop")
|> JS.add_class("modal-spring-in", to: "#dialog-id-content")
%>
<%=
JS.add_class("backdrop-fade-out", to: "#dialog-id-backdrop")
|> JS.add_class("modal-spring-out", to: "#dialog-id-content")
|> JS.hide(to: "#dialog-id", time: 200)
|> JS.remove_class("backdrop-fade-in backdrop-fade-out", to: "#dialog-id-backdrop")
|> JS.remove_class("modal-spring-in modal-spring-out", to: "#dialog-id-content")
%>
Implementation notes:
style="display: none;").phx-click-loading:opacity-50 utility.JS.push/2.