user3273967
user3273967

Reputation:

Rails - Partial in another view can't access its controller

I'm trying to build a profile page that displays posts sent only to the requested user, and allows the visitor to write a post of their own. Because this simplified example should have two distinct controllers: users and posts, I made partials for each post action to render within the user's show action.

Directory structure for my views directory looks like this:

- posts
  - _index.html.erb
  - _new.html.erb

- users
  - show.html.erb
  ... (etc.)

Section that displays these partials within the user's show.html.erb:

<section>
    <h3>Posts:</h3>
    <%= render '/posts/new', :post => Post.new %>
    <%= render '/posts/index', :posts => Post.where(target_id: params[:id]) %>
</section>

I eventually found out that you could pass variables into the partial in this render line, and though this works, it's very messy and probably doesn't follow the best practices.

Ideally, I'd want these partials to be connected with the posts controller so I can write more complex database queries in a place that isn't the view:

class PostsController < ApplicationController
def new
    @post = Post.new
end

def index
    @posts = Post.where(target_id: params[:id])
end

def create
    @post = Post.new(post_params)   
    @post.user_id = current_user.id
    @post.target_id = params[:post][:target_id]

    if @post.save
        redirect_to :back, notice: 'You published a post!'
    else
        render new
    end
end

private
    def post_params
        params.require(:post).permit(:body)
    end
end

Currently, I haven't found a way of doing this. I know this is a newb question, but thanks for any help in advance.

Upvotes: 1

Views: 1425

Answers (2)

Rob Wise
Rob Wise

Reputation: 5120

I agree somewhat with @Mori's advice. As he said, you are trying to put too much logic into the controller. I think this was a result of you trying to get it out of the view, which is the right idea, but you want business logic to be in the model.

Also, those index and new actions for PostsController are never going to be called. When you are calling the render posts/new for example, that is rendering the view, not the controller action. So, those controller actions have no reason to exist.

I would implement the fix in perhaps a different way than Mori described. It's a recommended practice to try and pass as few instance variables from the controller to the view as possible (see 3rd bullet in the linked section).

Since it's really the show action of the UsersController we are talking about here, I as someone trying to understand your code would assume the instance variable you are passing to the show view is something like @user.

You may want to use an includes method when instantiating the @user object. The includes statement will allow you to load the additional models you will need to instantiate using the minimum number of queries possible (preventing an N+1 query situation). You probably don't want to load every single one if there are thousands of matching posts, so I put an arbitrary limit of 10 on that.

UsersController

def show
  @user = User.find(params[:id]).includes(:received_posts).limit(10)
end

#....

View

<section>
    <h3>Posts:</h3>
    <% unless @user.id == current_user.id %>
      <%= render 'posts/form', post: Post.new(user_id: @user.id) %>
    <% end %>
    <%= render @user.received_posts %>
</section>

Putting the partial for a new post instead as a view called posts/form will allow you to reuse that form if you want to render an edit action (form_for knows which action to use on submit by calling the passed model's persisted? method).

Note that this code assumes the User model has the second relationship with posts set up to be called received_posts, but you can change it to whatever reflects the reality. By passing the received_posts collection to the render method, Rails is smart enough to know that if you want to render a collection of Post models to look for a posts/_post partial and render one for each Post. It's a little cleaner looking IMO. Just make sure to move your posts/show code into that. posts/show implies this is its own action and not something used as a partial for something else.

Upvotes: 0

Mori
Mori

Reputation: 27779

You are attempting to treat your controllers like models: doing the post work in post controller and the user work in user controller. But controllers are task-oriented, not model-oriented.

Since you want posts info in your user form, it's typical to gather it in the user controller. E.g.

class UsersController < ApplicationController
  def show
    ...
    @posts = Post.where(user_id: user.id)
  end
end

That @posts instance variable is visible in the show template and any partials it calls. But many coders prefer to send it explicitly through render arguments, as more functional:

<%= render '/posts/post_list', posts: @posts %>

For one thing it's easier to refactor when you can see at a glance all of the partial's dependencies.

Upvotes: 1

Related Questions