A LiveComponent version of the DynamicForm renderer with automatic state management.
This component handles form state, validation, and submission automatically, communicating with the parent LiveView via message passing.
Attributes
Required
:id- Component ID (string, required by LiveComponent):instance- DynamicForm.Instance struct, JSON string, or map containing form configuration
Optional
:on_change- 1-arity function(payload) -> payloadrun after the built-in validations on every change and during the submit validation pass (default:nil; see "Lifecycle callbacks" below):on_submit- 1-arity function(payload) -> payloadrun on every submit — valid or not (default:nil; see "Lifecycle callbacks" below):on_success- 1-arity function(payload)run on every valid submission instead of the default{:dynamic_form, payload}message to the parent LiveView (default:nil; see "Messages" below):data- Initial form data for edit mode — existing record values; a payload'sdataround-trips directly (map, default:%{}):form_name- Form namespace for submitted params (string, default:"dynamic_form"):submit_text- Submit button text (string, default:"Submit", not required whenhide_submitistrue):hide_submit- Whether to hide the submit button (boolean, default:false):gettext- Gettext backend module for translations (atom, default:DynamicForm.Gettext):validation_summary- Display validation errors at top of form (string,nil,"simple", or"detailed", default:nil):components- Custom components module (e.g. the app's Phoenix-generated CoreComponents); functions it exports override the built-ins per function — seeDynamicForm.Components(atom, default:nil, falling back to the:dynamic_form, :componentsconfig):custom_field_types- Custom field types map (type name => Ecto type), merged over the:dynamic_form, :custom_field_typesconfig — seeDynamicForm.FieldTypes(map, default:nil)
Usage
Basic Usage
The component sends the parent LiveView a message on every valid submission — the parent performs the side effect:
<.live_component
module={DynamicForm.RendererLive}
id="contact-form"
instance={@form_instance}
/>
def handle_info({:dynamic_form, %DynamicForm.Payload{data: data}}, socket) do
{:ok, contact} = MyApp.Contacts.create_contact(data)
{:noreply, put_flash(socket, :info, "Created contact #{contact.id}")}
endCustom success handling
Define on_success to replace the default message with your own behavior
(a differently-shaped message, a PubSub broadcast, or nothing at all):
<.live_component
module={DynamicForm.RendererLive}
id="contact-form"
instance={@form_instance}
on_success={fn payload -> send(self(), {:contact_saved, payload.data}) end}
/>Edit Mode
Pre-populate the form with existing data:
<.live_component
module={DynamicForm.RendererLive}
id="user-profile"
instance={@form_instance}
data={%{"name" => "John", "email" => "john@example.com"}}
form_name="user_profile"
/>Disabled Fields
Fields can be marked as disabled: true in the form instance configuration.
Disabled fields are displayed but cannot be edited by the user.
Important: Disabled HTML fields are not submitted by browsers, so their values
are automatically preserved by merging the initial :data with form submissions.
This ensures disabled field values remain in the changeset throughout validation
and submission.
External Submit Button
You can place a submit button outside the form element by using the hide_submit
option and DynamicForm.RendererLive.submit_button/1:
<DynamicForm.RendererLive.submit_button form="my-form-form">
Save Changes
</DynamicForm.RendererLive.submit_button>
<.live_component
module={DynamicForm.RendererLive}
id="my-form"
instance={@form_instance}
hide_submit={true}
/>Note: The form ID is automatically generated as "#{id}-form", so if your component
ID is "my-form", the form element ID will be "my-form-form".
Lifecycle callbacks: on_change and on_submit
Both hooks mirror the form's phx-change/phx-submit events. Each is a
1-arity function that receives a DynamicForm.Payload — carrying the
changeset after the built-in validations and the applied data — and
returns the payload, transformed or untouched. Callbacks are for
validation, not side effects: perform actions (database writes,
navigation) in the parent's handle_info/2 instead.
on_change extends validation: it runs after the built-in validations on
every change (and during the submit validation pass). Errors added via
DynamicForm.Payload.add_error/4 render inline live, exactly like
built-in validations. Keep it cheap — it runs per keystroke.
on_submit runs on every submit — valid or not — so it can batch
expensive checks (API calls, database lookups) with the built-in errors
into one complete error list:
<.live_component
module={DynamicForm.RendererLive}
id="contact-form"
instance={@form_instance}
on_submit={&MyApp.Contacts.verify/1}
/>
def verify(payload) do
case verify_phone_number(payload.data[:phone]) do # expensive, submit-only
{:ok, normalized} ->
DynamicForm.Payload.put_extra(payload, :normalized_phone, normalized)
:error ->
DynamicForm.Payload.add_error(payload, :phone, "is not a valid phone number")
end
endMessages
By default, the component sends the parent LiveView a message on every valid submission:
{:dynamic_form, %DynamicForm.Payload{}}Invalid submissions never message the parent — their errors render inline
on the form. The payload delivered is the one returned by the callbacks
(or built by the component when none are configured), so anything stashed
in :extra is available in handle_info/2.
Defining on_success replaces the default message: the function is
called with the payload on every valid submission and no
{:dynamic_form, payload} message is sent. Its return value is ignored.
Use it to send a custom message, broadcast over PubSub, or make the form
fully self-contained.
Summary
Functions
Renders a submit button that can be placed outside a form element.
Functions
Renders a submit button that can be placed outside a form element.
Uses the HTML form attribute to associate the button with a form by its ID.
This allows the submit button to be placed anywhere on the page, not just
inside the form element.
When using with DynamicForm.RendererLive, the form ID is automatically
generated as "#{component_id}-form". For example, if your LiveComponent
has id="my-form", the form element ID will be "my-form-form".
Examples
# LiveComponent with external submit button
<DynamicForm.RendererLive.submit_button form="contact-form-form">
Submit Contact Form
</DynamicForm.RendererLive.submit_button>
<.live_component
module={DynamicForm.RendererLive}
id="contact-form"
instance={@form_instance}
hide_submit={true}
/>
# In a modal footer
<.modal id="edit-modal">
<.live_component
module={DynamicForm.RendererLive}
id="user-profile"
instance={@form_instance}
hide_submit={true}
/>
<:actions>
<DynamicForm.RendererLive.submit_button form="user-profile-form">
Save Profile
</DynamicForm.RendererLive.submit_button>
</:actions>
</.modal>Attributes
form- The ID of the form element to submit (required)class- Additional CSS classes to apply to the buttondisabled- Whether the button is disabled
Attributes
form(:string) (required) - The ID of the form element to submit.class(:string) - Additional CSS classes. Defaults tonil.disabled(:boolean) - Whether the button is disabled. Defaults tofalse.- Global attributes are accepted. Supports all globals plus:
["name", "value"].
Slots
inner_block(required)