Phoenix Resource: new/ create with dynamic association

Hi there!

I’ve used phoenix.gen.html to build a UserAccount resource. A UserAccount can either belong to a Client or an Employee, so I need to add one of these associations to the account.

I’m not sure not what’s the best way to do it. I think I need to add the information to the URL, so that I can add a button “Create UserAccount” to an employee’s or a client’s page.

It might be easier to understand in code:

defmodule App.UserAccountController do
  use App.Web, :controller

  alias App.{Client, Employee, UserAccount}

  def new(conn, _params) do

    # Would it make sense to build an association here already?
    # I've tried that an error:
    # protocol Phoenix.HTML.FormData not implemented for %App.UserAccount...
    changeset = UserAccount.changeset(%UserAccount{})
    render(conn, "new.html", changeset: changeset)
  end

  def create(conn, %{"user_account" => user_account_params}) do

    # Owner can be a Client or an Employee
    # How can I get its type and id?
    owner = Client
    owner = Employee

    user_account = Repo.one!(owner, owner_id) |> build_assoc(:user_account)

    changeset = UserAccount.changeset(user_account, user_account_params)

    case Repo.insert(changeset) do
      # ...
    end
  end
end

I hope it’s clear what I want to achieve, if not I’ll try to explain it differently :yum:

Thanks!

Edit: I just found the following and think that I could use query params as a simple solution, but then I’d need to build the association in the new function already. How could I do that? (http://www.phoenixframework.org/docs/routing#section-more-on-path-helpers)

I think I’ve found a solution. It would be great though if you could tell me, if there’s a better way :slight_smile:

1 - Add hidden inputs to the form

<%= hidden_input f, :client_id %>
<%= hidden_input f, :employee_id %>

2 - Add changeset to allow these values

def create_changeset(struct, params) do
  struct
  |> changeset(params)
  |> cast(params, [:client_id, :employee_id])
end

3 - Add scrub_params plug to the controller

plug :scrub_params, "user_account" when action in [:create, :update]

4 - Adjust controller

def new(conn, %{"client_id" => id}), do: new(conn, Client, id)
def new(conn, %{"employee_id" => id}), do: new(conn, Employee, id)

defp new(conn, model, id) do
  user_account = Repo.get!(model, id) |> build_assoc(:user_account)

  changeset = UserAccount.changeset(user_account)
  render(conn, "new.html", changeset: changeset)
end

def create(conn, %{"user_account" => user_account_params}) do
  changeset = UserAccount.create_changeset(%UserAccount{}, user_account_params)
  # ...
end

I always like to post solutions just in case someone else has a similar problem, hope I didn’t forget something :slight_smile: If someone knows how I could improve my solution, I’m happy to hear from you!

2 Likes

Honestly I’d have a single user that have relationships to both client and employee tables that can point back to the user. That way a user could be a client, and employee, or both, or potentially others that you may add down the line.

I don’t really see how this is different from my solution? Currently I have client_id and employee_id columns on my user_accounts table. I’ve added a constraint to make sure that exactly one of them is set. This way I can simply add a *_id column to the table, edit the constraint and allow other resources to create user accounts and log in as well.

The way I stated is the ‘other’ way, have the client and employee tables have a user_id link. You can set a constraint so only one is allowed if you only want one, but this allows it to scale better instead of potentially adding more columns on user (I keep my user table from pointing to anything, everything else points to ‘it’, something I’d picked up over the past couple decades for who-knows-what-reason).

1 Like

In your controller code:

def new(conn, %{"client_id" => id}), do: new(conn, Client, id)
def new(conn, %{"employee_id" => id}), do: new(conn, Employee, id)

defp new(conn, model, id) do
   ...
end

Do the first 2 new/2 functions know that the private new/3 function is the one they want to call in their body because of the number of arguments they pass to it?

Correct. a function is defined as both a name and an arity. Just new defines nothing, there are actually two functions there and both of their ID’s are new/2 and new/3, and since the do: bodies of both new/2 functions call new with 3 arguments, they know to call new/3

1 Like