Forms
Hybrid Form
A LiveView form with a React control inside it
- forms
- Phoenix.HTML.Form encoder
Key concepts
A React component can sit inside an ordinary <.simple_form>
alongside HEEx inputs, as long as it renders a field named the way
Phoenix.HTML.Form
expects. It doesn't need pushEvent
at all — the field's value reaches phx-change
the same
way any native input's would.
import * as React from "react";
import { useState } from "react";
import { DualRangeSlider } from "../ui/dual-range-slider";
import { Label } from "../ui/label";
function formatMilliseconds(ms) {
const seconds = Math.floor(ms / 1000) % 60;
const minutes = Math.floor(ms / (1000 * 60)) % 60;
const hours = Math.floor(ms / (1000 * 60 * 60));
const parts = [];
if (hours > 0) parts.push(`${hours}h`);
if (minutes > 0) parts.push(`${minutes}m`);
if (seconds > 0 || parts.length === 0) parts.push(`${seconds}s`);
return parts.join("");
}
// A LiveView form field, wired up like any other React control: it doesn't
// call pushEvent itself. `inputName` gives Radix's slider the field name
// Phoenix.HTML.Form expects, so the value reaches phx-change like a normal
// form field would.
export function HybridForm({ inputName, value, min, max, step }) {
const [values, setValues] = useState(value);
return (
<div className="flex w-full flex-col space-y-8">
<Label>Delay between</Label>
<DualRangeSlider
name={inputName}
label={(v) => <span>{formatMilliseconds(v ?? 0)}</span>}
value={values}
onValueChange={setValues}
min={min}
max={max}
step={step}
/>
</div>
);
}
How it works
The slider passes inputName="settings[delay_between]"
straight through to Radix's name
prop, which renders
hidden inputs under that name for each thumb. The browser submits
them like any other form field, so phx-change="validate"
fires exactly as it would for a native <input>.
The component's own dragging state — the thumb positions mid-drag —
lives in useState and never touches the server; only a
real change event submits a value, same as a native range input.