Basic Nested Resource New and Create Actions?

greetings Phoenix community,

I am creating a new project with classic nested resources

Router

    resources "/channels", ChannelController do
      resources "/posts", PostController
    end

I am now modifying post_controller.ex and am having issues with new and create, index is OK

  def index(conn, %{"channel_id" => channel_id}) do
    channel = Channels.get_channel!(channel_id)
    render(conn, "index.html", channel: channel)
  end

not sure I have new correct

  def new(conn, %{"channel_id" => id}) do
    channel = Channels.get_channel!(id)
    changeset = Channels.change_post(%Post{})
    render(conn, "new.html", changeset: changeset, channel: channel)
  end

The new form renders and when I post I get the create error
new.html.heex

<h1>New Post</h1>

<%= render "form.html", changeset: @changeset,
                        action: Routes.channel_post_path(@conn, :create, @channel) %>

<span><%= link "Back", to: Routes.channel_path(@conn, :index) %></span>

and create I think I get an error

  def create(conn, params = %{"channel_id" => id, "post" => post_params}) do
    case Channels.create_post(params) do
      {:ok, post} ->
        conn
        |> put_flash(:info, "Post created successfully.")
        |> redirect(to: Routes.post_path(conn, :show, post))

      {:error, %Ecto.Changeset{} = changeset} ->
        render(conn, "new.html", changeset: changeset)
    end
  end

get error

ArgumentError at POST /channels/1/posts
assign @channel not available in template

and iex

Request: POST /channels/1/posts
** (exit) an exception was raised:
    ** (ArgumentError) assign @channel not available in template.

Please make sure all proper assigns have been set. If you are
calling a component, make sure you are passing all required
assigns as arguments.

Available assigns: [:changeset, :conn, :current_user]

I couldn’t see any documentation on modifying the new and create for nested resources, which seems like it should be a foundation use case.
https://hexdocs.pm/phoenix/routing.html#nested-resources

apologies if I have missed something obvious

thankyou

FWIW I re-used a pattern from Phoenix 1.4 days

I would be interested to learn if there is a newer, preferred style guide

def index(conn, %{"channel_id" => channel_id}) do
    channel = Channels.get_channel!(channel_id)
    render(conn, "index.html", channel: channel)
  end

  def new(conn, %{"channel_id" => id}) do
    channel = Channels.get_channel!(id)
    changeset = Channels.change_post(%Post{})
    render(conn, "new.html", changeset: changeset, channel: channel)
  end

  def create(conn, params = %{"post" => post_params, "channel_id" => channel_id}) do
    channel      = Repo.get!(Channel, channel_id) |> Repo.preload(:posts)
    changeset = channel
      |> Ecto.build_assoc(:posts)
      |> Post.changeset(post_params)

    case Repo.insert(changeset) do
      {:ok, _post} ->
        conn
        |> put_flash(:info, "Post created successfully!")
        |> redirect(to: Routes.channel_post_path(conn, :show, channel, channel.post))
# IEx.pry
      # {:error, changeset} ->
      #   render(conn, PostView, "show.html", channel: channel, post: channel.post, post_changeset: changeset)
    end
  end

  def show(conn, %{"id" => id, "channel_id" => channel_id}) do
    channel = Channels.get_channel!(channel_id)
    post = Channels.get_post!(id)
    render(conn, "show.html", post: post, channel: channel)
  end

  def edit(conn, %{"id" => id, "channel_id" => channel_id}) do
    channel = Channels.get_channel!(channel_id)
    post = Channels.get_post!(id)
    changeset = Channels.change_post(post)
    render(conn, "edit.html", post: post, changeset: changeset, channel: channel)
  end

  def update(conn, %{"id" => id, "channel_id" => channel_id, "post" => post_params}) do
    channel = Channels.get_channel!(channel_id)
    post = Channels.get_post!(id)
    changeset = Post.changeset(post, post_params)
    case Repo.update(changeset) do
      {:ok, post} ->
        conn
        |> put_flash(:info, "Post updated successfully.")
        |> redirect(to: Routes.channel_post_path(conn, :show, channel, post))

      {:error, %Ecto.Changeset{} = changeset} ->
        render(conn, "edit.html", post: post, changeset: changeset)
    end
  end

  def delete(conn, %{"id" => id, "channel_id" => channel_id}) do
    channel = Channels.get_channel!(channel_id)
    Repo.get!(Post, id) |> Repo.delete!
    conn
    |> put_flash(:info, "Deleted channel post!")
    |> redirect(to: Routes.channel_post_path(conn, :index, channel))
  end

I prefer to do this in the controller action, when using nested resource.

  def action(conn, _) do
    channel = Channels.get_channel!(conn.params["channel_id"])
    args = [conn, conn.params, channel]
    apply(__MODULE__, action_name(conn), args)
  end

and then… all my nested actions take the parent as an additional parameter.

thanks @kokolegorille, I’ve tried that also

how do your controllers look?

I guess there is no way to do this nested resource pattern without diverging from the generator styles

feels like a good candidate for some kind of core PR to allow this to work ootb

It looks like this.

  def new(conn, _params, event) do
    changeset = Core.change_document(Ecto.build_assoc(event, :documents))
    render(conn, "new.html", changeset: changeset, event: event)
  end

  def create(conn, %{"document" => document_params} = _params, event) do
    case Core.create_document(event, document_params) do
      {:ok, _document} ->
        conn
        |> log_success()
        |> put_flash(:info, gettext("Document created successfully."))
        |> redirect(to: Routes.admin_event_path(conn, :show, event.permalink))

      {:error, %Ecto.Changeset{} = changeset} ->
        conn
        |> log_failed()
        |> put_flash(:error, gettext("Could not create document."))
        |> render("new.html", changeset: changeset, event: event)
    end
  end

  def delete(conn, %{"id" => id}, event) do
    document = Core.get_document(id)
    {:ok, _document} = Core.delete_document(document)

    conn
    |> log_success()
    |> put_flash(:info, gettext("Document deleted successfully."))
    |> redirect(to: Routes.admin_event_path(conn, :show, event.permalink))
  end

with some functions defined elsewhere… and using a permalink as client facing id.

I also don’t use generators, but write my controllers.

2 Likes

This error is because the two ways that new.html gets rendered don’t pass the same assigns:

# in the new action
render(conn, "new.html", changeset: changeset, channel: channel)

# in the create action
render(conn, "new.html", changeset: changeset)

I’d recommend you refactor Channels.create_post to take a channel struct and then the rest of params, something like:

  def create(conn, params = %{"channel_id" => id, "post" => post_params}) do
    channel = Channels.get_channel!(id)
    case Channels.create_post(channel, params) do
      {:ok, post} ->
        conn
        |> put_flash(:info, "Post created successfully.")
        |> redirect(to: Routes.post_path(conn, :show, post))

      {:error, %Ecto.Changeset{} = changeset} ->
        render(conn, "new.html", changeset: changeset, channel: channel)
    end
  end