Skip to content

Instantly share code, notes, and snippets.

@clemens
Last active August 29, 2015 14:10
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save clemens/d27dcfba9ae437110fb8 to your computer and use it in GitHub Desktop.
Save clemens/d27dcfba9ae437110fb8 to your computer and use it in GitHub Desktop.
Problems without using instance variables to hand data from a Rails controller to a view

This is the first time I've run into an issue like this.

I've been using focused_controller in some projects and in others I've just extracted the expose method (see below). Everything was always fine.

Now in a CRM app I have a CommentsController that handles comments for multiple commentable resources – e.g. a contact. The comment form is currently always displayed on the resource's show page. So in case of errors (e.g. empty comment text), the commentable's show template is the one that needs to be re-rendered in order to properly show error messages. I don't want to do ugly stuff like redirect with error messages in the GET parameters or put something in the session.

As it stands, I found two solutions that actually work and allow me to use contact (as a method call) in the show template:

  • Use a combination of instance_variable_set/get/defined?. Remember: the commentable is essentially polymorphic so I have to set the name dynamically.
  • Pass a :locals hash to the render call.

I don't really mind the :locals approach – but at the same time I feel this should be easier – e.g. by using an instance-level expose call.

# thanks, @jonleighton
# https://github.com/jonleighton/focused_controller/blob/master/lib/focused_controller/mixin.rb#L24
module Expose
extend ActiveSupport::Concern
module ClassMethods
def expose(name, &block)
if block_given?
define_method(name) do |*args|
ivar = "@#{name}"
if instance_variable_defined?(ivar)
instance_variable_get(ivar)
else
instance_variable_set(ivar, instance_exec(block, *args, &block))
end
end
else
attr_reader name
end
helper_method name
end
end
end
module ApplicationHelper
def method_missing(name, *args, &block)
ivar = :"@#{name}"
return super unless instance_variable_defined?(ivar)
instance_variable_get(ivar)
end
end
class CommentsController < ApplicationController
load_and_authorize_resource :prepend => true, :through => :commentable
expose(:comment) { commentable.model.comments.find(params[:id]) }
def create
comment = commentable.comments.build(comment_params)
authorize!(:create, comment)
if comment.save
flash[:notice] = 'Comment successfully added!'
redirect_to commentable
else
instance_variable_set(:"@#{comment.commentable_type.underscore}", commentable)
flash.now[:alert] = 'Comment could not be added!'
render "#{comment.commentable_type.tableize}/show"
end
end
end
class CommentsController < ApplicationController
load_and_authorize_resource :prepend => true, :through => :commentable
expose(:comment) { commentable.model.comments.find(params[:id]) }
def create
comment = commentable.comments.build(comment_params)
authorize!(:create, comment)
if comment.save
flash[:notice] = 'Comment successfully added!'
redirect_to commentable
else
flash.now[:alert] = 'Comment could not be added!'
render "#{comment.commentable_type.tableize}/show", :locals => { comment.commentable_type.underscore.to_sym => commentable }
end
end
end
# Pseudo-code – this is what I'd like to be able to do.
class CommentsController < ApplicationController
load_and_authorize_resource :prepend => true, :through => :commentable
expose(:comment) { commentable.model.comments.find(params[:id]) }
def create
comment = commentable.comments.build(comment_params)
authorize!(:create, comment)
if comment.save
flash[:notice] = 'Comment successfully added!'
redirect_to commentable
else
expose(comment.commentable_type.underscore, commentable)
flash.now[:alert] = 'Comment could not be added!'
render "#{comment.commentable_type.tableize}/show"
end
end
end
@Odaeus
Copy link

Odaeus commented Dec 4, 2014

So the problem is that you are rendering the show view for a resource, e.g. contact, and understandably using the name of the resource in the view code rather than a generic term.

I think :locals is the most correct approach in this situation. I probably would have structured my controllers differently to avoid the situation. For example by having a controller for each commentable resource that inherits from a base commentable controller but has its own expose method for the resource. So your route would be /contacts/23/comments/ and a ContactCommentsController < CommentsController. Because I'd prefer to load the resource model directly then build the comment on it rather than the other way around. Also I often need to do more to render the "show" page of a resource than just loading a single model. Can be nicely solved with AJAX too.

Sorry probably not very helpful!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment