View Source AshPhoenix.Form (ash_phoenix v2.1.12)
A module to allow you to fluidly use resources with Phoenix forms.
Life cycle
The general workflow is, with either LiveView or Phoenix forms:
- Create a form with
AshPhoenix.Form
- Render the form with
Phoenix.Component.form
(orCoreComponents.simple_form
), or, if using Surface,<Form>
- To validate the form (e.g with
phx-change
for liveview), pass the submitted params toAshPhoenix.Form.validate/3
- On form submission, pass the params to
AshPhoenix.Form.submit/2
- On success, use the result to redirect or assign. On failure, reassign the provided form.
The following keys exist on the form to show where in the lifecycle you are:
submitted_once?
- If the form has ever been submitted. Useful for not showing any errors on the first attempt to fill out a form.just_submitted?
- If the form has just been submitted and no validation has happened since. Useful for things like triggering a UI effect that should stop when the form is modified again..changed?
- If something about the form is different than it originally was. Note that in some cases this can yield a false positive, specifically if a nested form is removed and then a new one is added with the exact same values..touched_forms
- A MapSet containing all keys in the form that have been modified. When submitting a form, only these keys are included in the parameters.
Forms in the code interface
Throughout this documentation you will see forms created with AshPhoenix.Form.for_create/3
and other functions like it.
This is perfectly fine to do, however there is a way to use AshPhoenix.Form
in a way that adds clarity to its usage
and makes it easier to find usage of each action. Code interfaces allow us to do this for standard action calls, i.e:
resources do
resource MyApp.Accounts.User do
define :register_with_password, args: [:email, :password]
define :update_user, action: :update, args: [:email, :password]
end
end
Adding the AshPhoenix
extension to our domains and resources, like so:
use Ash.Domain,
extensions: [AshPhoenix]
will cause another function to be generated for each definition, beginning with form_to_
.
With this extension, the standard setup for forms looks something like this:
def render(assigns) do
~H"""
<.form for={@form} phx-change="validate" phx-submit="submit">
<.input field={@form[:email]} />
<.input field={@form[:password]} />
<.button type="submit" />
</.form>
"""
end
def mount(_params, _session, socket) do
# Here we call our new generated function to create the form
{:ok, assign(socket, form: MyApp.Accounts.form_to_register_with_password())}
end
def handle_event(socket, "validate", %{"form" => params}) do
form = AshPhoenix.Form.validate(socket.assigns.form, params)
{:noreply, assign(socket, :form, form)}
end
def handle_event(socket, "submit", %{"form" => params}) do
case AshPhoenix.Form.submit(socket.assigns.form, params: params) do
{:ok, _user} ->
socket =
socket
|> put_flash(:success, "User registered successfully")
|> push_navigate(to: ~p"/")
{:noreply, socket}
{:error, form} ->
socket =
socket
|> put_flash(:error, "Something went wrong")
|> assign(:form, form)
{:noreply, socket}
end
end
Working with related data
If your resource action accepts related data, (for example a managed relationship argument, or an embedded resource attribute), you can
use Phoenix's inputs_for
for that field, but you must do one of two things:
- Tell AshPhoenix.Form to automatically derive this behavior from your action, for example:
form =
user
|> AshPhoenix.Form.for_update(:update, forms: [auto?: true])
|> to_form()
- Explicitly configure the behavior of it using the
forms
option. Seefor_create/3
for more.
For example:
form =
user
|> AshPhoenix.Form.for_update(:update,
forms: [
profile: [
resource: MyApp.Profile,
data: user.profile,
create_action: :create,
update_action: :update
forms: [
emails: [
data: user.profile.emails,
resource: MyApp.UserEmail,
create_action: :create,
update_action: :update
]
]
]
])
|> to_form()
Working with compound types
Compound types, such as Ash.Money
, will need some extra work to make it work.
For instance, when working with the Transfer
type in AshDoubleEntry.Transfer
, it will have the Ash.Money
type for amount
. When rendering the forms, you should do as follows:
<.input
name={@form[:amount].name <> "[amount]"}
id={@form[:amount].id <> "_amount"}
label="Amount"
value={if(@form[:amount].value, do: @form[:amount].value.amount)}
/>
<.input
type="select"
name={@form[:amount].name <> "[currency]"}
id={@form[:amount].id <> "_currency"}
options={[:USD, :HKD, :EUR]}
label="Currency"
value={if(@form[:amount].value, do: @form[:amount].value.currency)}
/>
The above will allow the fields to be used by the AshPhoenix.Form
when creating or updating a Transfer.
You can follow the same style with other compound types.
LiveView
AshPhoenix.Form
(unlike ecto changeset based forms) expects to be reused throughout the lifecycle of the liveview.
You can use Phoenix events to add and remove form entries and submit/2
to submit the form, like so:
def render(assigns) do
~H"""
<.simple_form for={@form} phx-change="validate" phx-submit="submit">
<%!-- Attributes for the parent resource --%>
<.input type="email" label="Email" field={@form[:email]} />
<%!-- Render nested forms for related data --%>
<.inputs_for :let={item_form} field={@form[:items]}>
<.input type="text" label="Item" field={item_form[:name]} />
<.input type="number" label="Amount" field={item_form[:amount]} />
<.button type="button" phx-click="remove_form" phx-value-path={item_form.name}>
Remove
</.button>
</.inputs_for>
<:actions>
<.button type="button" phx-click="add_form" phx-value-path={@form[:items].name}>
Add Item
</.button>
<.button>Save</.button>
</:actions>
</.simple_form>
"""
end
def mount(_params, _session, socket) do
form =
MyApp.Grocery.Order
|> AshPhoenix.Form.for_create(:create,
forms: [
items: [
type: :list,
resource: MyApp.Grocery.Item,
create_action: :create
]
]
)
|> AshPhoenix.Form.add_form([:items])
|> to_form()
{:ok, assign(socket, form: form)}
end
# In order to use the `add_form` and `remove_form` helpers, you
# need to make sure that you are validating the form on change
def handle_event("validate", %{"form" => params}, socket) do
form = AshPhoenix.Form.validate(socket.assigns.form, params)
{:noreply, assign(socket, form: form)}
end
def handle_event("submit", %{"form" => params}, socket) do
case AshPhoenix.Form.submit(socket.assigns.form, params: params) do
{:ok, order} ->
{:noreply,
socket
|> put_flash(:info, "Saved order for #{order.email}!")
|> push_navigate(to: ~p"/")}
{:error, form} ->
{:noreply, assign(socket, form: form)}
end
end
def handle_event("add_form", %{"path" => path}, socket) do
form = AshPhoenix.Form.add_form(socket.assigns.form, path)
{:noreply, assign(socket, form: form)}
end
def handle_event("remove_form", %{"path" => path}, socket) do
form = AshPhoenix.Form.remove_form(socket.assigns.form, path)
{:noreply, assign(socket, form: form)}
end
Summary
Functions
Adds an error to the source underlying the form.
Adds a new form at the provided path.
A utility to get the list of arguments the action underlying the form accepts
A utility to get the list of attributes the action underlying the form accepts
Clears a given input's value on a form.
Returns the errors on the form.
Creates a form corresponding to any given action on a resource.
Creates a form corresponding to a create action on a resource.
Creates a form corresponding to a destroy action on a record.
Creates a form corresponding to a read action on a resource.
Creates a form corresponding to an update action on a record.
Gets the form at the specified path
Returns true if a given form path exists in the form
Returns the hidden fields for a form as a keyword list
Toggles the form to be ignored or not ignored.
Returns true if the form is ignored
Merge the new options with the saved options on a form. See update_options/2
for more.
Returns the parameters from the form that would be submitted to the action.
A utility for parsing paths of nested forms in query encoded format.
Removes a form at the provided path.
Sets the data of the form, in addition to the data of the underlying source, if applicable.
Submits the form.
Same as submit/2
, but raises an error if the submission fails.
Mark a field or fields as touched
Updates the form at the provided path using the given function.
Updates the list of forms matching a given path. Does not validate that the path points at a single form like update_form/4
.
Update the saved options on a form.
Update the previous params provided to the form, and revalidate.
Validates the parameters against the form.
Gets the value for a given field in the form.
Types
@type source() :: Ash.Changeset.t() | Ash.Query.t() | Ash.Resource.record()
@type t() :: %AshPhoenix.Form{ action: atom(), added?: term(), any_removed?: term(), changed?: term(), data: nil | Ash.Resource.record(), domain: term(), errors: boolean(), form_keys: Keyword.t(), forms: map(), id: term(), just_submitted?: boolean(), method: String.t(), name: term(), opts: Keyword.t(), original_data: term(), params: map(), prepare_params: term(), prepare_source: nil | (source() -> source()), raw_params: term(), resource: Ash.Resource.t(), source: source(), submit_errors: Keyword.t() | nil, submitted_once?: boolean(), touched_forms: term(), transform_errors: nil | (source(), error :: Ash.Error.t() -> [ {field :: atom(), message :: String.t(), substituations :: Keyword.t()} ]), transform_params: nil | (map() -> term()), type: :create | :update | :destroy | :read, valid?: boolean(), warn_on_unhandled_errors?: term() }
Functions
Adds an error to the source underlying the form.
This can be used for adding errors from different sources to a form. Keep in mind, if they don't match
a field on the form (typically extracted via the field
key in the error), they won't be displayed by default.
Ensure that the errors
field of the form is set to true
if you want the errors to be visible.
See Ash.Error.to_ash_error/3
for more on supported values for error
.
Options
:path
- The path to add the error to. If the error(s) already have a path, don't specify a path yourself.
@spec add_form(t(), path(), Keyword.t()) :: t()
@spec add_form(Phoenix.HTML.Form.t(), path(), Keyword.t()) :: Phoenix.HTML.Form.t()
Adds a new form at the provided path.
Doing this requires that the form has a create_action
and a resource
configured.
path
can be one of two things:
- A list of atoms and integers that lead to a form in the
forms
option provided.[:posts, 0, :comments]
to add a comment to the first post. - The html name of the form, e.g
form[posts][0][comments]
to mimic the above
If you pass parameters to this function, keep in mind that, unless they are string keyed in
the same shape they might come from your form, then the result of params/1
will reflect that,
i.e add_form(form, "foo", params: %{bar: 10})
, could produce params like %{"field" => value, "foo" => [%{bar: 10}]}
.
Notice how they are not string keyed as you would expect. However, once the form is changed (in liveview) and a call
to validate/2
is made with that input, then the parameters would become what you'd expect. In this way, if you are using
add_form
with not string keys/values you may not be able to depend on the shape of the params
map (which you should ideally
not depend on anyway).
:prepend
(boolean/0
) - If specified, the form is placed at the beginning of the list instead of the end of the list The default value isfalse
.:params
(term/0
) - The initial parameters to add the form with. The default value is%{}
.:validate?
(boolean/0
) - Validates the new full form. The default value istrue
.:validate_opts
(term/0
) - Options to pass tovalidate
. Only used ifvalidate?
is set totrue
(the default) The default value is[]
.:type
- Iftype
is set to:read
, the form will be created for a read action. A hidden field will be set in the form called_form_type
to track this information. Valid values are :read, :create, :update, :destroy The default value is:create
.:data
(term/0
) - The data to set backing the form. Generally you'd only want to do this if you are adding a form withtype: :read
additionally.
A utility to get the list of arguments the action underlying the form accepts
A utility to get the list of attributes the action underlying the form accepts
@spec can_submit?(t()) :: boolean()
@spec can_submit?(Phoenix.HTML.Form.t()) :: boolean()
Clears a given input's value on a form.
Accepts a field (atom) or a list of fields (atoms) as a second argument.
@spec ensure_can_submit!(t()) :: t()
@spec ensure_can_submit!(Phoenix.HTML.Form.t()) :: Phoenix.HTML.Form.t()
@spec errors(t() | Phoenix.HTML.Form.t(), Keyword.t()) :: ([{atom(), {String.t(), Keyword.t()}}] | [String.t()] | [{atom(), String.t()}]) | %{ required(list()) => [{atom(), {String.t(), Keyword.t()}}] | [String.t()] | [{atom(), String.t()}] }
Returns the errors on the form.
By default, only errors on the form being passed in (not nested forms) are provided.
Use for_path
to get errors for nested forms.
:format
- Values:- `:raw` - `[field:, {message, substitutions}}]` (for translation) - `:simple` - `[field: "message w/ variables substituted"]` - `:plaintext` - `["field: message w/ variables substituted"]`
Valid values are :simple, :raw, :plaintext The default value is
:simple
.:for_path
(term/0
) - The path of the form you want errors for, either as a list or as a string, e.g[:comments, 0]
orform[comments][0]
Passing:all
will cause this function to return a map of path to its errors, like so:%{[:comments, 0] => [body: "is invalid"], ...}
The default value is[]
.
Creates a form corresponding to any given action on a resource.
If given a create, read, update, or destroy action, the appropriate for_*
function will be called instead. So use this function when you don't know
the type of the action, or it is a generic action.
Options
:actor
(term/0
) - The actor performing the action. Passed through to the underlying action.:forms
(keyword/0
) - Nested form configurations. Seefor_create/3
"Nested Form Options" docs for more.:warn_on_unhandled_errors?
(boolean/0
) - Warns on any errors that don't match the form pattern of{:field, "message", [replacement: :vars]}
or implement theAshPhoenix.FormData.Error
protocol. The default value istrue
.:domain
(atom/0
) - The domain to use when calling the action.:as
(String.t/0
) - The name of the form in the submitted params. You will need to pull the form params out using this key. The default value is"form"
.:id
(String.t/0
) - The html id of the form. Defaults to the value of:as
if provided, otherwise "form":transform_errors
(term/0
) - Allows for manual manipulation and transformation of errors.
If possible, try to implementAshPhoenix.FormData.Error
for the error (if it as a custom one, for example). If that isn't possible, you can provide this function which will get the changeset and the error, and should return a list of ash phoenix formatted errors, e.g[{field :: atom, message :: String.t(), substituations :: Keyword.t()}]
:prepare_source
- A 1-argument function the receives the initial changeset (or query) and makes any relevant changes to it. This can be used to do things like:- Set default argument values before the validations are run using
Ash.Changeset.set_arguments/2
orAsh.Changeset.set_argument/3
- Set changeset context
- Do any other pre-processing on the changeset
- Set default argument values before the validations are run using
:prepare_params
- A 2-argument function that receives the params map and the :validate atom and should return prepared params. Called before the form is validated.:transform_params
- A function for post-processing the form parameters before they are used for changeset validation/submission. Use a 3-argument function to pattern match on theAshPhoenix.Form
struct.:method
(String.t/0
) - The http method to associate with the form. Defaults topost
for creates, andput
for everything else.:exclude_fields_if_empty
- These fields will be ignored if they are empty strings.
This list of fields supports dead view forms. When a form is submitted from dead view empty fields are submitted as empty strings. This is problematic for fields that allow_nil or those that have default values.:tenant
(term/0
) - The current tenant. Passed through to the underlying action.
Any additional options will be passed to the underlying call to build the source, i.e
Ash.ActionInput.for_action/4
, or Ash.Changeset.for_*
. This means you can set things
like the tenant/actor. These will be retained, and provided again when Form.submit/3
is called.
Nested Form Options
:type
- The cardinality of the nested form -:list
or:single
. Valid values are :list, :single The default value is:single
.:sparse?
(boolean/0
) - If the nested form issparse
, the form won't expect all inputs for all forms to be present.
Has no effect if the type is:single
.
Normally, if you leave some forms out of a list of nested forms, they are removed from the parameters passed to the action. For example, if you had apost
with two comments[%Comment{id: 1}, %Comment{id: 2}]
and you passed down params likecomments[0][id]=1&comments[1][text]=new_text
, we would remove the second comment from the input parameters, resulting in the following being passed into the action:%{"comments" => [%{"id" => 1, "text" => "new"}]}
. By setting it to sparse, you have to explicitly useremove_form
for that removal to happen. So in the same scenario above, the parameters that would be sent would actually be%{"comments" => [%{"id" => 1, "text" => "new"}, %{"id" => 2}]}
.
One major difference withsparse?
is that the form actually ignores the index provided, e.gcomments[0]...
, and instead uses the primary key e.gcomments[0][id]
to match which form is being updated. This prevents you from having to find the index of the specific item you want to update. Which could be very gnarly on deeply nested forms. If there is no primary key, or the primary key does not match anything, it is treated as a new form.
REMEMBER: You need to usePhoenix.Components.inputs_for
to render the nested forms, or manually add hidden inputs usinghidden_inputs_for
(orHiddenInputs
if using Surface) for the id to be automatically placed into the form.:forms
(keyword/0
) - Forms nested inside the current nesting level in all cases.:for_type
- What action types the form applies for. Leave blank for it to apply to all action types. Valid values are :read, :create, :update:merge?
(boolean/0
) - When building parameters, this input will be merged with its parent input. This allows for combining multiple forms into a single input. The default value isfalse
.:for
(atom/0
) - When creating parameters for the action, the key that the forms should be gathered into. Defaults to the key used to configure the nested form. Ignored ifmerge?
istrue
.:resource
(atom/0
) - The resource of the nested forms. Unnecessary if you are providing thedata
key, and not adding additional forms to this path.:create_action
(atom/0
) - The create action to use when building new forms. Only necessary if you want to useadd_form/3
with this path.:update_action
(atom/0
) - The update action to use when building forms for data. Only necessary if you supply thedata
key.:data
(term/0
) - The current value or values that should have update forms built by default.
You can also provide a single argument function that will return the data based on the data of the parent form. This is important for multiple nesting levels of:list
type forms, because the data depends on which parent is being rendered.
@spec for_create(Ash.Resource.t(), action :: atom(), opts :: Keyword.t()) :: t()
Creates a form corresponding to a create action on a resource.
Options
Options not listed below are passed to the underlying call to build the changeset/query, i.e Ash.Changeset.for_create/4
:actor
(term/0
) - The actor performing the action. Passed through to the underlying action.:forms
(keyword/0
) - Nested form configurations. Seefor_create/3
"Nested Form Options" docs for more.:warn_on_unhandled_errors?
(boolean/0
) - Warns on any errors that don't match the form pattern of{:field, "message", [replacement: :vars]}
or implement theAshPhoenix.FormData.Error
protocol. The default value istrue
.:domain
(atom/0
) - The domain to use when calling the action.:as
(String.t/0
) - The name of the form in the submitted params. You will need to pull the form params out using this key. The default value is"form"
.:id
(String.t/0
) - The html id of the form. Defaults to the value of:as
if provided, otherwise "form":transform_errors
(term/0
) - Allows for manual manipulation and transformation of errors.
If possible, try to implementAshPhoenix.FormData.Error
for the error (if it as a custom one, for example). If that isn't possible, you can provide this function which will get the changeset and the error, and should return a list of ash phoenix formatted errors, e.g[{field :: atom, message :: String.t(), substituations :: Keyword.t()}]
:prepare_source
- A 1-argument function the receives the initial changeset (or query) and makes any relevant changes to it. This can be used to do things like:- Set default argument values before the validations are run using
Ash.Changeset.set_arguments/2
orAsh.Changeset.set_argument/3
- Set changeset context
- Do any other pre-processing on the changeset
- Set default argument values before the validations are run using
:prepare_params
- A 2-argument function that receives the params map and the :validate atom and should return prepared params. Called before the form is validated.:transform_params
- A function for post-processing the form parameters before they are used for changeset validation/submission. Use a 3-argument function to pattern match on theAshPhoenix.Form
struct.:method
(String.t/0
) - The http method to associate with the form. Defaults topost
for creates, andput
for everything else.:exclude_fields_if_empty
- These fields will be ignored if they are empty strings.
This list of fields supports dead view forms. When a form is submitted from dead view empty fields are submitted as empty strings. This is problematic for fields that allow_nil or those that have default values.:tenant
(term/0
) - The current tenant. Passed through to the underlying action.
Nested Form Options
To automatically determine the nested forms available for a given form, use forms: [auto?: true]
.
You can add additional nested forms by including them in the forms
config alongside auto?: true
.
See the module documentation of AshPhoenix.Form.Auto
for more information. If you want to do some
manipulation of the auto forms, you can also call AshPhoenix.Form.Auto.auto/2
, and then manipulate the
result and pass it to the forms
option. To pass options, use auto?: [option1: :value]
. See the
documentation of AshPhoenix.Form.Auto
for more.
:type
- The cardinality of the nested form -:list
or:single
. Valid values are :list, :single The default value is:single
.:sparse?
(boolean/0
) - If the nested form issparse
, the form won't expect all inputs for all forms to be present.
Has no effect if the type is:single
.
Normally, if you leave some forms out of a list of nested forms, they are removed from the parameters passed to the action. For example, if you had apost
with two comments[%Comment{id: 1}, %Comment{id: 2}]
and you passed down params likecomments[0][id]=1&comments[1][text]=new_text
, we would remove the second comment from the input parameters, resulting in the following being passed into the action:%{"comments" => [%{"id" => 1, "text" => "new"}]}
. By setting it to sparse, you have to explicitly useremove_form
for that removal to happen. So in the same scenario above, the parameters that would be sent would actually be%{"comments" => [%{"id" => 1, "text" => "new"}, %{"id" => 2}]}
.
One major difference withsparse?
is that the form actually ignores the index provided, e.gcomments[0]...
, and instead uses the primary key e.gcomments[0][id]
to match which form is being updated. This prevents you from having to find the index of the specific item you want to update. Which could be very gnarly on deeply nested forms. If there is no primary key, or the primary key does not match anything, it is treated as a new form.
REMEMBER: You need to usePhoenix.Components.inputs_for
to render the nested forms, or manually add hidden inputs usinghidden_inputs_for
(orHiddenInputs
if using Surface) for the id to be automatically placed into the form.:forms
(keyword/0
) - Forms nested inside the current nesting level in all cases.:for_type
- What action types the form applies for. Leave blank for it to apply to all action types. Valid values are :read, :create, :update:merge?
(boolean/0
) - When building parameters, this input will be merged with its parent input. This allows for combining multiple forms into a single input. The default value isfalse
.:for
(atom/0
) - When creating parameters for the action, the key that the forms should be gathered into. Defaults to the key used to configure the nested form. Ignored ifmerge?
istrue
.:resource
(atom/0
) - The resource of the nested forms. Unnecessary if you are providing thedata
key, and not adding additional forms to this path.:create_action
(atom/0
) - The create action to use when building new forms. Only necessary if you want to useadd_form/3
with this path.:update_action
(atom/0
) - The update action to use when building forms for data. Only necessary if you supply thedata
key.:data
(term/0
) - The current value or values that should have update forms built by default.
You can also provide a single argument function that will return the data based on the data of the parent form. This is important for multiple nesting levels of:list
type forms, because the data depends on which parent is being rendered.
@spec for_destroy(Ash.Resource.record(), action :: atom(), opts :: Keyword.t()) :: t()
Creates a form corresponding to a destroy action on a record.
Options:
:actor
(term/0
) - The actor performing the action. Passed through to the underlying action.:forms
(keyword/0
) - Nested form configurations. Seefor_create/3
"Nested Form Options" docs for more.:warn_on_unhandled_errors?
(boolean/0
) - Warns on any errors that don't match the form pattern of{:field, "message", [replacement: :vars]}
or implement theAshPhoenix.FormData.Error
protocol. The default value istrue
.:domain
(atom/0
) - The domain to use when calling the action.:as
(String.t/0
) - The name of the form in the submitted params. You will need to pull the form params out using this key. The default value is"form"
.:id
(String.t/0
) - The html id of the form. Defaults to the value of:as
if provided, otherwise "form":transform_errors
(term/0
) - Allows for manual manipulation and transformation of errors.
If possible, try to implementAshPhoenix.FormData.Error
for the error (if it as a custom one, for example). If that isn't possible, you can provide this function which will get the changeset and the error, and should return a list of ash phoenix formatted errors, e.g[{field :: atom, message :: String.t(), substituations :: Keyword.t()}]
:prepare_source
- A 1-argument function the receives the initial changeset (or query) and makes any relevant changes to it. This can be used to do things like:- Set default argument values before the validations are run using
Ash.Changeset.set_arguments/2
orAsh.Changeset.set_argument/3
- Set changeset context
- Do any other pre-processing on the changeset
- Set default argument values before the validations are run using
:prepare_params
- A 2-argument function that receives the params map and the :validate atom and should return prepared params. Called before the form is validated.:transform_params
- A function for post-processing the form parameters before they are used for changeset validation/submission. Use a 3-argument function to pattern match on theAshPhoenix.Form
struct.:method
(String.t/0
) - The http method to associate with the form. Defaults topost
for creates, andput
for everything else.:exclude_fields_if_empty
- These fields will be ignored if they are empty strings.
This list of fields supports dead view forms. When a form is submitted from dead view empty fields are submitted as empty strings. This is problematic for fields that allow_nil or those that have default values.:tenant
(term/0
) - The current tenant. Passed through to the underlying action.
Any additional options will be passed to the underlying call to Ash.Changeset.for_destroy/4
. This means
you can set things like the tenant/actor. These will be retained, and provided again when Form.submit/3
is called.
@spec for_read(Ash.Resource.t(), action :: atom(), opts :: Keyword.t()) :: t()
Creates a form corresponding to a read action on a resource.
Options:
:actor
(term/0
) - The actor performing the action. Passed through to the underlying action.:forms
(keyword/0
) - Nested form configurations. Seefor_create/3
"Nested Form Options" docs for more.:warn_on_unhandled_errors?
(boolean/0
) - Warns on any errors that don't match the form pattern of{:field, "message", [replacement: :vars]}
or implement theAshPhoenix.FormData.Error
protocol. The default value istrue
.:domain
(atom/0
) - The domain to use when calling the action.:as
(String.t/0
) - The name of the form in the submitted params. You will need to pull the form params out using this key. The default value is"form"
.:id
(String.t/0
) - The html id of the form. Defaults to the value of:as
if provided, otherwise "form":transform_errors
(term/0
) - Allows for manual manipulation and transformation of errors.
If possible, try to implementAshPhoenix.FormData.Error
for the error (if it as a custom one, for example). If that isn't possible, you can provide this function which will get the changeset and the error, and should return a list of ash phoenix formatted errors, e.g[{field :: atom, message :: String.t(), substituations :: Keyword.t()}]
:prepare_source
- A 1-argument function the receives the initial changeset (or query) and makes any relevant changes to it. This can be used to do things like:- Set default argument values before the validations are run using
Ash.Changeset.set_arguments/2
orAsh.Changeset.set_argument/3
- Set changeset context
- Do any other pre-processing on the changeset
- Set default argument values before the validations are run using
:prepare_params
- A 2-argument function that receives the params map and the :validate atom and should return prepared params. Called before the form is validated.:transform_params
- A function for post-processing the form parameters before they are used for changeset validation/submission. Use a 3-argument function to pattern match on theAshPhoenix.Form
struct.:method
(String.t/0
) - The http method to associate with the form. Defaults topost
for creates, andput
for everything else.:exclude_fields_if_empty
- These fields will be ignored if they are empty strings.
This list of fields supports dead view forms. When a form is submitted from dead view empty fields are submitted as empty strings. This is problematic for fields that allow_nil or those that have default values.:tenant
(term/0
) - The current tenant. Passed through to the underlying action.
Any additional options will be passed to the underlying call to Ash.Query.for_read/4
. This means
you can set things like the tenant/actor. These will be retained, and provided again when Form.submit/3
is called.
Keep in mind that the source
of the form in this case is a query, not a changeset. This means that, very likely,
you would not want to use nested forms here. However, it could make sense if you had a query argument that was an
embedded resource, so the capability remains.
Nested Form Options
:type
- The cardinality of the nested form -:list
or:single
. Valid values are :list, :single The default value is:single
.:sparse?
(boolean/0
) - If the nested form issparse
, the form won't expect all inputs for all forms to be present.
Has no effect if the type is:single
.
Normally, if you leave some forms out of a list of nested forms, they are removed from the parameters passed to the action. For example, if you had apost
with two comments[%Comment{id: 1}, %Comment{id: 2}]
and you passed down params likecomments[0][id]=1&comments[1][text]=new_text
, we would remove the second comment from the input parameters, resulting in the following being passed into the action:%{"comments" => [%{"id" => 1, "text" => "new"}]}
. By setting it to sparse, you have to explicitly useremove_form
for that removal to happen. So in the same scenario above, the parameters that would be sent would actually be%{"comments" => [%{"id" => 1, "text" => "new"}, %{"id" => 2}]}
.
One major difference withsparse?
is that the form actually ignores the index provided, e.gcomments[0]...
, and instead uses the primary key e.gcomments[0][id]
to match which form is being updated. This prevents you from having to find the index of the specific item you want to update. Which could be very gnarly on deeply nested forms. If there is no primary key, or the primary key does not match anything, it is treated as a new form.
REMEMBER: You need to usePhoenix.Components.inputs_for
to render the nested forms, or manually add hidden inputs usinghidden_inputs_for
(orHiddenInputs
if using Surface) for the id to be automatically placed into the form.:forms
(keyword/0
) - Forms nested inside the current nesting level in all cases.:for_type
- What action types the form applies for. Leave blank for it to apply to all action types. Valid values are :read, :create, :update:merge?
(boolean/0
) - When building parameters, this input will be merged with its parent input. This allows for combining multiple forms into a single input. The default value isfalse
.:for
(atom/0
) - When creating parameters for the action, the key that the forms should be gathered into. Defaults to the key used to configure the nested form. Ignored ifmerge?
istrue
.:resource
(atom/0
) - The resource of the nested forms. Unnecessary if you are providing thedata
key, and not adding additional forms to this path.:create_action
(atom/0
) - The create action to use when building new forms. Only necessary if you want to useadd_form/3
with this path.:update_action
(atom/0
) - The update action to use when building forms for data. Only necessary if you supply thedata
key.:data
(term/0
) - The current value or values that should have update forms built by default.
You can also provide a single argument function that will return the data based on the data of the parent form. This is important for multiple nesting levels of:list
type forms, because the data depends on which parent is being rendered.
@spec for_update(Ash.Resource.record(), action :: atom(), opts :: Keyword.t()) :: t()
Creates a form corresponding to an update action on a record.
Options:
:actor
(term/0
) - The actor performing the action. Passed through to the underlying action.:forms
(keyword/0
) - Nested form configurations. Seefor_create/3
"Nested Form Options" docs for more.:warn_on_unhandled_errors?
(boolean/0
) - Warns on any errors that don't match the form pattern of{:field, "message", [replacement: :vars]}
or implement theAshPhoenix.FormData.Error
protocol. The default value istrue
.:domain
(atom/0
) - The domain to use when calling the action.:as
(String.t/0
) - The name of the form in the submitted params. You will need to pull the form params out using this key. The default value is"form"
.:id
(String.t/0
) - The html id of the form. Defaults to the value of:as
if provided, otherwise "form":transform_errors
(term/0
) - Allows for manual manipulation and transformation of errors.
If possible, try to implementAshPhoenix.FormData.Error
for the error (if it as a custom one, for example). If that isn't possible, you can provide this function which will get the changeset and the error, and should return a list of ash phoenix formatted errors, e.g[{field :: atom, message :: String.t(), substituations :: Keyword.t()}]
:prepare_source
- A 1-argument function the receives the initial changeset (or query) and makes any relevant changes to it. This can be used to do things like:- Set default argument values before the validations are run using
Ash.Changeset.set_arguments/2
orAsh.Changeset.set_argument/3
- Set changeset context
- Do any other pre-processing on the changeset
- Set default argument values before the validations are run using
:prepare_params
- A 2-argument function that receives the params map and the :validate atom and should return prepared params. Called before the form is validated.:transform_params
- A function for post-processing the form parameters before they are used for changeset validation/submission. Use a 3-argument function to pattern match on theAshPhoenix.Form
struct.:method
(String.t/0
) - The http method to associate with the form. Defaults topost
for creates, andput
for everything else.:exclude_fields_if_empty
- These fields will be ignored if they are empty strings.
This list of fields supports dead view forms. When a form is submitted from dead view empty fields are submitted as empty strings. This is problematic for fields that allow_nil or those that have default values.:tenant
(term/0
) - The current tenant. Passed through to the underlying action.
Any additional options will be passed to the underlying call to Ash.Changeset.for_update/4
. This means
you can set things like the tenant/actor. These will be retained, and provided again when Form.submit/3
is called.
@spec get_form(t() | Phoenix.HTML.Form.t(), path()) :: t() | nil
Gets the form at the specified path
Returns true if a given form path exists in the form
Toggles the form to be ignored or not ignored.
To set this manually in an html form, use the field :_ignored
and set it
to the string "true". Any other value will not result in the form being ignored.
@spec ignored?(t() | Phoenix.HTML.Form.t()) :: boolean()
Returns true if the form is ignored
@spec merge_options(t(), Keyword.t()) :: t()
@spec merge_options(Phoenix.HTML.Form.t(), Keyword.t()) :: Phoenix.HTML.Form.t()
Merge the new options with the saved options on a form. See update_options/2
for more.
Returns the parameters from the form that would be submitted to the action.
This can be useful if you want to get the parameters and manipulate them/build a custom changeset afterwards.
@spec parse_path!(t() | Phoenix.HTML.Form.t(), path(), opts :: Keyword.t()) :: [atom() | integer()] | no_return()
A utility for parsing paths of nested forms in query encoded format.
For example:
parse_path!(form, "post[comments][0][sub_comments][0])
[:comments, 0, :sub_comments, 0]
@spec remove_form(t(), path(), Keyword.t()) :: t()
@spec remove_form(Phoenix.HTML.Form.t(), path(), Keyword.t()) :: Phoenix.HTML.Form.t()
Removes a form at the provided path.
See add_form/3
for more information on the path
argument.
If you are not using liveview, and you want to support removing forms that were created based on the data
option from the browser, you'll need to include in the form submission a custom list of strings to remove, and
then manually iterate over them in your controller, for example:
Enum.reduce(removed_form_paths, form, &AshPhoenix.Form.remove_form(&2, &1))
Sets the data of the form, in addition to the data of the underlying source, if applicable.
Queries do not track data (because that wouldn't make sense), so this will not update the data for read actions
@spec submit(t(), Keyword.t()) :: {:ok, Ash.Resource.record() | nil | [Ash.Notifier.Notification.t()]} | {:ok, Ash.Resource.record(), [Ash.Notifier.Notification.t()]} | :ok | {:error, t()}
@spec submit(Phoenix.HTML.Form.t(), Keyword.t()) :: {:ok, Ash.Resource.record() | nil | [Ash.Notifier.Notification.t()]} | {:ok, Ash.Resource.record(), [Ash.Notifier.Notification.t()]} | :ok | {:error, Phoenix.HTML.Form.t()}
Submits the form.
If the submission returns an error, the resulting form can be rerendered. Any nested errors will be passed down to the corresponding form for that input.
Options:
:force?
(boolean/0
) - Submit the form even if it is invalid in its current state. The default value isfalse
.:action_opts
(keyword/0
) - Opts to pass to the call to Ash when calling the action. The default value is[]
.:errors
(boolean/0
) - Wether or not to show errors after submitting. The default value istrue
.:override_params
(term/0
) - If specified, then the params are not extracted from the form.
How this different fromparams
: providingparams
is simply results in callingvalidate(form, params)
before proceeding. The values that are passed into the action are then extracted from the form usingparams/2
. Withoverride_params
, the form is not validated again, and theoverride_params
are passed directly into the action.:params
(term/0
) - If specified,validate/3
is called with the new params before submitting the form.
This is a shortcut to avoid needing to explicitly validate before every submit.
For example:form |> AshPhoenix.Form.validate(params) |> AshPhoenix.Form.submit()
Is the same as:
form |> AshPhoenix.Form.submit(params: params)
:read_one?
(boolean/0
) - If submitting a read form, a single result will be returned (via read_one) instead of a list of results.
Ignored for non-read forms. The default value isfalse
.:before_submit
(function of arity 1) - A function to apply to the source (changeset or query) just before submitting the action. Must return the modified changeset.
@spec submit!(t(), Keyword.t()) :: Ash.Resource.record() | :ok | no_return()
Same as submit/2
, but raises an error if the submission fails.
Mark a field or fields as touched
To mark nested fields as touched use with update_form/4
or update_forms_at_path/4
Updates the form at the provided path using the given function.
Marks all forms along the path as touched by default. To prevent it, provide mark_as_touched?: false
.
This can be useful if you have a button that should modify a nested form in some way, for example.
Updates the list of forms matching a given path. Does not validate that the path points at a single form like update_form/4
.
Additionally, if it gets to a list of child forms and the next part of the path is not an integer, it will update all of the forms at that path.
Update the saved options on a form.
When a form is created, options like actor
and authorize?
are stored in the opts
key.
If you have a case where these options change over time, for example a select box that determines the actor, use this function to override those opts.
You may want to validate again after this has been changed if it can change the results of your form validation.
@spec update_params(t(), fun :: (map() -> map()), validate_opts :: Keyword.t()) :: t()
@spec update_params( Phoenix.HTML.Form.t(), params :: (map() -> map()), validate_opts :: Keyword.t() ) :: Phoenix.HTML.Form.t()
Update the previous params provided to the form, and revalidate.
Accepts the same options as validate/2
, passing them through directly.
You should prefer to use validate/2
when you have all of the params from the form.
This is meant for cases when some event has occured that should modify the params,
not as a replacement for validate/2
.
This can be useful for things like customized inputs or buttons, that have special
handlers in your live view. For example, if you have an appointment that expresses
a list of available times in the UI, but the action just takes a single time
argument,
you can make each available time a button, like so:
<.button phx-click="time-selected" phx-value-time="<%= time %>" />
and then have an event handler like this:
def handle_event("time-selected", %{"time" => time}, socket) do
form = AshPhoenix.Form.update_params(socket.assigns.form, &Map.put(&1, "time", time))
{:noreply, assign(socket, :form, form)}
end
@spec validate(t(), map(), Keyword.t()) :: t()
@spec validate(Phoenix.HTML.Form.t(), map(), Keyword.t()) :: Phoenix.HTML.Form.t()
Validates the parameters against the form.
Options:
:errors
(boolean/0
) - Set to false to hide errors after validation. The default value istrue
.:target
(list ofString.t/0
) - The_target
param provided by phoenix. Used to support theonly_touched?
option.:only_touched?
(boolean/0
) - If set to true, only fields that have been marked as touched will be used
If you use this for validation you likely want to use it when submitting as well. The default value isfalse
.
@spec value(t() | Phoenix.HTML.Form.t(), atom()) :: any()
Gets the value for a given field in the form.