How to get transaction result of previous forms data of a resource and pass through next resource in AshPhoenix.Form.Submit?

Hello,

I have a form that consists of multiple resources to submit. AshPhoenix.Form helped to render nested forms easily using add_form and submit using AshPhoenix.Form.Submit. But i need some help regarding, getting the pervious value of the form. Please go through these details, this is a registration form that has a flow like this:

  1. Register user
  2. Get the id from the step 1, and add it to user_personal_details (Action already accepts user_id) along with other params like first_name, last_name etc in the resource
  3. Register Company.
  4. Add company id from 3 to update registered user in 1 with company_id.

I have setup all the resources, domains and default actions with relationships. This kind of flow used to do it in a transaction with ecto’s Multi.New() function. But I wonder if I can simplify this process using the existing function AshPhoenix.Form.Submit or any other Ash’s way. Please let me know I need to share source code.

Thank you.

You can store data in the changeset’s context key, which is deep merged with Ash.Changeset.set_context. So you could have a change that adds a before_action hook, and that hook returns a changeset with context set that will be available to future hooks. Would be useful to see what your action looks like currently to advise more specifically.

Sorry I didn’t understand properly, also if there is some other better way for this please let me know. My bad, This are the resources:

  1. User Resource from ash authentication:
# added these extra attributes role and company_info_id in relationships:
validations do
    validate attribute_in(:role, [:admin, :user])
  end

  attributes do
    uuid_primary_key :id

    attribute :email, :ci_string do
      allow_nil? false
      public? true
    end

    attribute :hashed_password, :string do
      allow_nil? false
      sensitive? true
    end

    attribute :role, :atom do
      default :admin
    end
  end

  relationships do
    belongs_to :company_info, Mgx.Company.CompanyInfo

    has_one :personal_information, Mgx.Accounts.PersonalInformation
  end

#action:
create :register_with_password do
      description "Register a new user with a email and password."

      argument :email, :ci_string do
        allow_nil? false
      end

      argument :password, :string do
        description "The proposed password for the user, in plain text."
        allow_nil? false
        constraints min_length: 8
        sensitive? true
      end

      argument :password_confirmation, :string do
        description "The proposed password for the user (again), in plain text."
        allow_nil? false
        sensitive? true
      end

      # Sets the email from the argument
      change set_attribute(:email, arg(:email))

      # Hashes the provided password
      change AshAuthentication.Strategy.Password.HashPasswordChange

      # Generates an authentication token for the user
      change AshAuthentication.GenerateTokenChange

      # validates that the password matches the confirmation
      validate AshAuthentication.Strategy.Password.PasswordConfirmationValidation

      metadata :token, :string do
        description "A JWT that can be used to authenticate the user."
        allow_nil? false
      end
    end
  1. User Personal Resources
 actions do
    defaults [:read, :destroy, create: :*, update: :*]
    default_accept [:user_id,:first_name, :last_name, :country_code, :phone_number, :id_number]
  end

  attributes do
    uuid_v7_primary_key :id

    attribute :first_name, :string do
      allow_nil? false
    end

    attribute :last_name, :string do
      allow_nil? false
    end

    attribute :country_code, :string do
      allow_nil? false
    end

    attribute :phone_number, :string do
      allow_nil? false
    end

    attribute :id_number, :string do
      allow_nil? false
    end
  end

  relationships do
    belongs_to :user, Mgx.Accounts.User do
      allow_nil? false
    end
  end
  1. Company Resource
actions do
    defaults [:read, :destroy, create: :*, update: :*]
    default_accept [:name, :registration_number]
  end

  validations do
    validate attribute_in(:subscription_type, [:lngx, :gasx, :lngx_and_gasx])
  end

  attributes do
    uuid_v7_primary_key :id

    attribute :name, :string

    attribute :registration_number, :ci_string do
      allow_nil? false
    end

    attribute :subscription_type, :atom do
      description "The Subscrioption type. Must be one lngx, gasx or lngx and gasx. Check validations"
    end
  end

  relationships do
    has_many :user, Mgx.Accounts.User
  end

  identities do
    identity :unique_registration_number, [:registration_number] do
      eager_check? true
    end
  end

The actual form i am rendering is:

<.simple_form
      for={@form}
      id="registration_form"
      phx-submit="save"
      phx-change="validate"
      method="get"
    >
      <.inputs_for :let={personal_info} field={@form[:personal_information]}>
        <div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
          <.input field={personal_info[:first_name]} type="text" label="First Name" />
          <.input field={personal_info[:last_name]} type="text" label="Last Name" />
        </div>
        <.input field={personal_info[:id_number]} type="text" label="NRIC/Passport Number" />
        <div>
          <label for="phone-number" class="text-sm leading-6 text-black mb-2 font-semibold">
            Phone Number
          </label>
          <div class="grid grid-cols-[1fr_1.5fr] gap-4">
            <.input field={personal_info[:country_code]} type="select" options={@countries} />
            <.input field={personal_info[:phone_number]} type="text" />
          </div>
        </div>
      </.inputs_for>

      <.inputs_for :let={company_information} field={@form[:company_information]}>
        <.input field={company_information[:company_name]} type="text" label="Company Name" />
        <.input
          field={company_information[:company_registration_number]}
          type="text"
          label="Company Registration Number"
        />
      </.inputs_for>

      <.input field={@form[:email]} type="email" label="Email" required />

      <:actions>
        <.button phx-disable-with="Creating account..." class="w-full">Confirm</.button>
      </:actions>
    </.simple_form>

The Mount function:

def mount(_params, _session, socket) do
    form =
      AshPhoenix.Form.for_create(
        Mgx.Accounts.User,
        :register_with_password,
        forms: [
          personal_information: [
            resource: Mgx.Accounts.PersonalInformation,
            create_action: :create
          ],
          company_information: [
            resource: Mgx.Company.CompanyInfo,
            create_action: :create
          ]
        ]
      )
      |> AshPhoenix.Form.add_form([:personal_information])
      |> AshPhoenix.Form.add_form([:company_information])
      |> to_form()

    countries = Mgx.Utils.CountryCodes.get_countries()

    socket =
      socket
      |> assign(form: form, countries: countries)

    {:ok, socket}
  end

  def handle_event("save", %{"form" => params}, socket) do
  end

  def handle_event("validate", %{"form" => params}, socket) do
    form = AshPhoenix.Form.validate(socket.assigns.form, params)
    {:noreply, assign(socket, form: form)}
  end

Ah, I see what you mean. Using multiple explicit forms like that only really helps with the form structure itself. You should first be looking at how to implement this logic in your actions, and then work on the form logic, which likely would not require those nested forms once you are ready. Unfortunately I don’t have time to go through your entire code and craft an answer for your specific case, but I can provide a basic example.

# on your user resource

create :register do
  argument :company_name, :string, allow_nil?: false

  change fn changeset, _ -> 
    Ash.Changeset.before_action(changeset, fn changeset, _ -> 
      org = create_organization(changeset.arguments.company_name)
      Ash.Changeset.force_change_attributes(changeset, :company_id, company.id)
    end)
  end
end

Using “hooks” (Ash.Changeset.before_action, Ash.Changeset.after_action, etc.) allow us to build progressively more complex multi-step actions. Additionally, we can use arguments to accept additional inputs that don’t map directly to attributes to be changed.

With that, you can create a single form with all the inputs you need, no need to define nested forms.

Using manage_relationship

Given that what you are doing is essentially creating related data as part of an action, there is a builtin that can help with this, called manage_relationship.

You can use it like so:

create :register do
  argument :personal_information, :map, allow_nil?: false
  argument :company_information, :map, allow_nil?: false

  change manage_relationship(:personal_information, type: :create)
  change manage_relationship(:personal_information, type: :create)
end

Then, when creating your form, you can use auto?: true, which will automatically detect the necessary nested forms. The two in combination should be all you need to accomplish your stated goal.

AshPhoenix.Form.for_create(
  Mgx.Accounts.User,
  :register_with_password,
  forms: [
    auto?: true
  ]
)
|> AshPhoenix.Form.add_form([:personal_information])
|> AshPhoenix.Form.add_form([:company_information])
|> to_form()
1 Like

I see, Now I understood the flow of it. Thank you. Much appreciated.

1 Like