View Source AshPhoenix.Form (ash_phoenix v0.7.7)

A module to allow you to fluidly use resources with phoenix forms.

The general workflow is, with either liveview or phoenix forms:

  1. Create a form with AshPhoenix.Form
  2. Render that form with Phoenix's form_for (or, if using surface, <Form>)
  3. To validate the form (e.g with on-change for liveview), pass the input to AshPhoenix.Form.validate(form, params)
  4. On form submission, pass the input to AshPhoenix.Form.validate(form, params) and then use AshPhoenix.Form.submit(form, ApiModule)

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:

  1. Tell AshPhoenix.Form to automatically derive this behavior from your action, for example:
form =
  user
  |> AshPhoenix.Form.for_update(:update,
    api: MyApi,
    forms: [auto?: true]
    ])
  1. Explicitly configure the behavior of it using the forms option. See for_create/3 for more.

For example:

form =
  user
  |> AshPhoenix.Form.for_update(:update,
    api: MyApi,
    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
          ]
        ]
      ]
    ])

liveview

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:

alias MyApp.MyApi.{Comment, Post}

def render(assigns) do
  ~L"""
  <%= f = form_for @form, "#", [phx_change: :validate, phx_submit: :save] %>
    <%= label f, :text %>
    <%= text_input f, :text %>
    <%= error_tag f, :text %>

    <%= for comment_form <- inputs_for(f, :comments) do %>
      <%= hidden_inputs_for(comment_form) %>
      <%= text_input comment_form, :text %>

      <%= for sub_comment_form <- inputs_for(comment_form, :sub_comments) do %>
        <%= hidden_inputs_for(sub_comment_form) %>
        <%= text_input sub_comment_form, :text %>
        <button phx-click="remove_form" phx-value-path="<%= sub_comment_form.name %>">Add Comment</button>
      <% end %>

      <button phx-click="remove_form" phx-value-path="<%= comment_form.name %>">Add Comment</button>
      <button phx-click="add_form" phx-value-path="<%= comment_form.name %>">Add Comment</button>
    <% end %>

    <button phx-click="add_form" phx-value-path="<%= comment_form.name %>">Add Comment</button>

    <%= submit "Save" %>
  </form>
  """
end

def mount(%{"post_id" => post_id}, _session, socket) do
  post =
    Post
    |> MyApp.MyApi.get!(post_id)
    |> MyApi.load!(comments: [:sub_comments])

  form = AshPhoenix.Form.for_update(post,
    api: MyApp.MyApi,
    forms: [
      comments: [
        resource: Comment,
        data: post.comments,
        create_action: :create,
        update_action: :update
        forms: [
          sub_comments: [
            resource: Comment,
            data: &(&1.sub_comments),
            create_action: :create,
            update_action: :update
          ]
        ]
      ]
    ])

  {: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)
  # You can also skip errors by setting `errors: false` if you only want to show errors on submit
  # form = AshPhoenix.Form.validate(socket.assigns.form, params, errors: false)

  {:ok, assign(socket, :form, form)}
end

def handle_event("save", _params, socket) do
  case AshPhoenix.Form.submit(socket.assigns.form) do
    {:ok, result} ->
      # Do something with the result, like redirect
    {:error, form} ->
      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}) do
  form = AshPhoenix.Form.remove_form(socket.assigns.form, path)
  {:noreply, assign(socket, :form, form)}
end

Link to this section Summary

Functions

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

Returns the errors on the form.

Calls the corresponding for_* function depending on the action type

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.

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.

Same as submit/2, but raises an error if the submission fails.

Submits the form by calling the appropriate function on the configured api.

Update the saved options on a form.

Validates the parameters against the form.

Gets the value for a given field in the form.

Link to this section Types

@type t() :: %AshPhoenix.Form{
  action: atom(),
  added?: term(),
  any_removed?: term(),
  api: term(),
  changed?: term(),
  data: nil | Ash.Resource.record(),
  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(),
  resource: Ash.Resource.t(),
  source: Ash.Changeset.t() | Ash.Query.t(),
  submit_errors: Keyword.t() | nil,
  submitted_once?: boolean(),
  touched_forms: term(),
  transform_errors:
    nil
    | (Ash.Changeset.t() | Ash.Query.t(), 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()
}

Link to this section Functions

Link to this function

add_form(form, path, opts \\ [])

View Source
@spec add_form(t(), String.t() | [atom() | integer()], Keyword.t()) :: 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:

  1. 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.
  2. 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", %{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 - If specified, the form is placed at the beginning of the list instead of the end of the list The default value is false.

  • :params - The initial parameters to add the form with. The default value is %{}.

  • :validate? - Validates the new full form. The default value is true.

  • :validate_opts - Options to pass to validate. Only used if validate? is set to true (the default) The default value is [].

  • :type - If type 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. The default value is :create.

  • :data - The data to set backing the form. Generally you'd only want to do this if you are adding a form with type: :read additionally.

A utility to get the list of arguments the action underlying the form accepts

Link to this function

arguments_changed?(form)

View Source

A utility to get the list of attributes the action underlying the form accepts

Link to this function

errors(form, opts \\ [])

View Source
@spec errors(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"] The default value is :simple.
  • :for_path - The path of the form you want errors for, either as a list or as a string, e.g [:comments, 0] or form[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 `[]`.
Link to this function

errors_for(form, path, type \\ :raw)

View Source
This function is deprecated. Use errors/2 instead.
@spec errors_for(
  t(),
  [atom() | integer()] | String.t(),
  type :: :simple | :raw | :plaintext
) ::
  [{atom(), {String.t(), Keyword.t()}}] | [String.t()] | map() | nil
Link to this function

for_action(resource_or_data, action, opts)

View Source

Calls the corresponding for_* function depending on the action type

Link to this function

for_create(resource, action, opts \\ [])

View Source
@spec for_create(Ash.Resource.t(), action :: atom(), opts :: Keyword.t()) :: t()

Creates a form corresponding to a create action on a resource.

Options:

  • :forms - Nested form configurations. See for_create/3 "Nested Form Options" docs for more.

  • :warn_on_unhandled_errors? - Warns on any errors that don't match the form pattern of {:field, "message", [replacement: :vars]} or implement the AshPhoenix.FormData.Error protocol. The default value is true.

  • :api - The api module to use for form submission. If not set, calls to Form.submit/2 will fail

  • :as - 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 - The html id of the form. Defaults to the value of :as if provided, otherwise "form"

  • :transform_errors - Allows for manual manipulation and transformation of errors.
    If possible, try to implement AshPhoenix.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_params - A function for pre-processing the form parameters before they are handled by the form.

  • :transform_params - A function for post-processing the form parameters before they are used for changeset validation/submission.

  • :method - The http method to associate with the form. Defaults to post for creates, and put for everything else.

Any additional options will be passed to the underlying call to Ash.Changeset.for_create/4. 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

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.

  • :type - The cardinality of the nested form. The default value is :single.

  • :sparse? - If the nested form is sparse, 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 a post with two comments [%Comment{id: 1}, %Comment{id: 2}] and you passed down params like comments[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 use remove_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 with sparse? is that the form actually ignores the index provided, e.g comments[0]..., and instead uses the primary key e.g comments[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 use hidden_inputs_for (or HiddenInputs if using surface) for the id to be automatically placed into the form.

  • :forms - 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.

  • :merge? - 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 is false.

  • :for - 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 if merge? is true.

  • :resource - The resource of the nested forms. Unnecessary if you are providing the data key, and not adding additional forms to this path.

  • :create_action - The create action to use when building new forms. Only necessary if you want to use add_form/3 with this path.

  • :update_action - The update action to use when building forms for data. Only necessary if you supply the data key.

  • :data - 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.

Link to this function

for_destroy(data, action, opts \\ [])

View Source
@spec for_destroy(Ash.Resource.record(), action :: atom(), opts :: Keyword.t()) :: t()

Creates a form corresponding to a destroy action on a record.

Options:

  • :forms - Nested form configurations. See for_create/3 "Nested Form Options" docs for more.

  • :warn_on_unhandled_errors? - Warns on any errors that don't match the form pattern of {:field, "message", [replacement: :vars]} or implement the AshPhoenix.FormData.Error protocol. The default value is true.

  • :api - The api module to use for form submission. If not set, calls to Form.submit/2 will fail

  • :as - 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 - The html id of the form. Defaults to the value of :as if provided, otherwise "form"

  • :transform_errors - Allows for manual manipulation and transformation of errors.
    If possible, try to implement AshPhoenix.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_params - A function for pre-processing the form parameters before they are handled by the form.

  • :transform_params - A function for post-processing the form parameters before they are used for changeset validation/submission.

  • :method - The http method to associate with the form. Defaults to post for creates, and put for everything else.

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.

Link to this function

for_read(resource, action, opts \\ [])

View Source
@spec for_read(Ash.Resource.t(), action :: atom(), opts :: Keyword.t()) :: t()

Creates a form corresponding to a read action on a resource.

Options:

  • :forms - Nested form configurations. See for_create/3 "Nested Form Options" docs for more.

  • :warn_on_unhandled_errors? - Warns on any errors that don't match the form pattern of {:field, "message", [replacement: :vars]} or implement the AshPhoenix.FormData.Error protocol. The default value is true.

  • :api - The api module to use for form submission. If not set, calls to Form.submit/2 will fail

  • :as - 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 - The html id of the form. Defaults to the value of :as if provided, otherwise "form"

  • :transform_errors - Allows for manual manipulation and transformation of errors.
    If possible, try to implement AshPhoenix.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_params - A function for pre-processing the form parameters before they are handled by the form.

  • :transform_params - A function for post-processing the form parameters before they are used for changeset validation/submission.

  • :method - The http method to associate with the form. Defaults to post for creates, and put for everything else.

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

Nested Form Options

  • :type - The cardinality of the nested form. The default value is :single.

  • :sparse? - If the nested form is sparse, 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 a post with two comments [%Comment{id: 1}, %Comment{id: 2}] and you passed down params like comments[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 use remove_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 with sparse? is that the form actually ignores the index provided, e.g comments[0]..., and instead uses the primary key e.g comments[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 use hidden_inputs_for (or HiddenInputs if using surface) for the id to be automatically placed into the form.

  • :forms - 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.

  • :merge? - 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 is false.

  • :for - 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 if merge? is true.

  • :resource - The resource of the nested forms. Unnecessary if you are providing the data key, and not adding additional forms to this path.

  • :create_action - The create action to use when building new forms. Only necessary if you want to use add_form/3 with this path.

  • :update_action - The update action to use when building forms for data. Only necessary if you supply the data key.

  • :data - 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.

Link to this function

for_update(data, action, opts \\ [])

View Source
@spec for_update(Ash.Resource.record(), action :: atom(), opts :: Keyword.t()) :: t()

Creates a form corresponding to an update action on a record.

Options:

  • :forms - Nested form configurations. See for_create/3 "Nested Form Options" docs for more.

  • :warn_on_unhandled_errors? - Warns on any errors that don't match the form pattern of {:field, "message", [replacement: :vars]} or implement the AshPhoenix.FormData.Error protocol. The default value is true.

  • :api - The api module to use for form submission. If not set, calls to Form.submit/2 will fail

  • :as - 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 - The html id of the form. Defaults to the value of :as if provided, otherwise "form"

  • :transform_errors - Allows for manual manipulation and transformation of errors.
    If possible, try to implement AshPhoenix.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_params - A function for pre-processing the form parameters before they are handled by the form.

  • :transform_params - A function for post-processing the form parameters before they are used for changeset validation/submission.

  • :method - The http method to associate with the form. Defaults to post for creates, and put for everything else.

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(), [atom() | integer()] | String.t()) :: t() | nil
@spec has_form?(t(), [atom() | integer()] | String.t()) :: boolean()
@spec ignore(t()) :: t()

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()) :: boolean()

Returns true if the form is ignored

Link to this function

merge_options(form, opts)

View Source

Merge the new options with the saved options on a form. See update_options/2 for more.

Link to this function

params(form, opts \\ [])

View Source

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.

Link to this function

parse_path!(form, original_path)

View Source

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]
Link to this function

remove_form(form, path, opts \\ [])

View Source

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

Link to this function

submit!(form, opts \\ [])

View Source
@spec submit!(t(), Keyword.t()) :: Ash.Resource.record() | :ok | no_return()

Same as submit/2, but raises an error if the submission fails.

Link to this function

submit(form, opts \\ [])

View Source
@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()}

Submits the form by calling the appropriate function on the configured api.

For example, a form created with for_update/3 will call api.update(changeset), where changeset is the result of passing the Form.params/3 into Ash.Changeset.for_update/4.

If the submission returns an error, the resulting form can simply be rerendered. Any nested errors will be passed down to the corresponding form for that input.

Options:

  • :force? - Submit the form even if it is invalid in its current state. The default value is false.

  • :api_opts - Opts to pass to the call to the api when submitting The default value is [].

  • :override_params - If specified, then the params are not extracted from the form.
    How this different from params: providing params is simply results in calling validate(form, params) before proceeding. The values that are passed into the action are then extracted from the form using params/2. With override_params, the form is not validated again, and the override_params are passed directly into the action.

  • :params - 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? - 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 is false.

  • :before_submit - A function to apply to the source (changeset or query) just before submitting the action. Must return the modified changeset.

Link to this function

update_form(form, path, func, opts \\ [])

View Source
Link to this function

update_options(form, fun)

View Source

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.

Link to this function

validate(form, new_params, opts \\ [])

View Source
@spec validate(t(), map(), Keyword.t()) :: t()

Validates the parameters against the form.

Options:

  • :errors - Set to false to hide errors after validation The default value is true.
@spec value(t(), atom()) :: any()

Gets the value for a given field in the form.