Keep the callback JS next to the markup — no demos.js, no bundler
getDateMetadataCallback, customStylesCallback, … — is a
client-side phx-hook concern. Every other example wires those in
priv/static/assets/demos.js. This page instead uses a
colocated hook (LiveView 1.1+): the hook JS lives inline in this
colocated_hooks_live.ex, right under the picker it drives.
runtime variant: the
<script :type={ColocatedHook} runtime> compiles to an inline
<script> that registers a window["phx_hook_…"] global —
the LiveView client discovers it with no bundler and no app.js change.
In a normal esbuild app you drop runtime, write
export default {…}, and import the generated
phoenix-colocated/<app> manifest into your LiveSocket hooks.
Weekends are $220 (red), weekdays $150 (green). The badge value comes from
getDateMetadataCallback; the color comes from the class it assigns plus the
CSS that customStylesCallback injects into the picker's shadow DOM. Both are
set by the .PriceBadges hook defined at the bottom of this template — the
picker keeps hook={true} for its own events, and the colocated hook
rides on a parent element (one phx-hook per element).
mounted() runs — so the badges pop in a moment after the grid is already on
screen. That's inherent to attaching callbacks from a client hook, not a cost of
colocation: a bundled demos.js hook would flash identically. The popup pickers
on Badges & Tooltips hide it because their
calendar isn't rendered until you open it (callbacks already set by then).
Below is the hook itself — the actual code this page runs, colocated in
colocated_hooks_live.ex (not fetched from demos.js):
<%!-- in your LiveView render/1 — hook rides on a PARENT element, picker keeps hook={true} --%>
<div id="pricing-cb" phx-hook=".PriceBadges">
<.web_daterangepicker id="colocated-pricing" hook={true}
selection_mode="range" visible_months_count={2} positioning_mode="inline" />
</div>
<%!-- runtime variant (no bundler): the body EVALUATES TO the hook object (no `export default`).
It compiles to an inline <script> that registers a window global the LV client discovers. --%>
<script :type={Phoenix.LiveView.ColocatedHook} name=".PriceBadges" runtime>
{
mounted() {
const el = this.el.querySelector("web-daterangepicker");
customElements.whenDefined("web-daterangepicker").then(() => {
el.getDateMetadataCallback = ({ date }) => { // ONE v2.0.0 context object
const price = (date.getDay() === 0 || date.getDay() === 6) ? 220 : 150;
const tier = price >= 200 ? "price-high" : "price-low";
return { badgeText: "$" + price, badgeClass: "badge-number " + tier };
};
el.customStylesCallback = () => // inject shadow-DOM CSS
".drp__badge-cell.price-high{background:#fee2e2;color:#991b1b}" +
".drp__badge-cell.price-low{background:#d1fae5;color:#065f46}";
});
}
}
</script>
# esbuild app instead? drop `runtime`, use `export default { … }`, and
# import { hooks } from "phoenix-colocated/my_app" into your LiveSocket hooks.
Same logic, different home: compare with priv/static/assets/demos.js, which
drives the Badges & Tooltips page.
(pick a date anywhere on this page)