Nikita  Luparev Nikita Luparev - 6 months ago 13
Ruby Question

Why do I get "undefined method 'model_name' "?

I'm doing a small experiment and trying to extract some logic from controller's action.

I have code like this:

Controller

def create
@user = User.new(user_params)
if @user.save
redirect_to user_url(@user), notice: 'Welcome to MyApp'
else
render :new
end
end


And try to create something like this:

Controller

def create
user_service.display
end

private

def user_service
RenderService.new(self)
end


Separate Service object

require 'forwardable'

class RenderService
extend Forwardable
delegate [:params, :user_url, :redirect_to, :render] => :@obj

attr_reader :obj, :user
def initialize(obj)
@obj = obj
@user = User.new(user_params)
end

def display
redirect_to(user_url(user), notice: 'Welcome to MyApp') if user.save
render(:new) unless user.save
end

private

def user_params
params.require(:user).permit(:name, :email, :password, :confirmation)
end
end


Form for creating new user

= simple_form_for(@user, html: { }) do |form|
= form.input :name, required: true
= form.input :email, required: true
= form.input :password, required: true
= form.input :confirmation, required: true
= form.submit 'Create my account', class: 'btn btn-primary'


As you can see all I tried to do is to encapsulate logic into a separate class, but I got an error from
ActionView::Template::Error
for some reason and the error says
undefined method 'model_name' for nil:NilClass
.

I really don't understand why it does not work. To me it looks like I sent the same message to the same object and it should work just fine, but it doesn't.

Second question: why is
ActionView
involved here?

Thank you for any explanation.

Oh, by the way, if you know the code responsible for such behavior and where it lives in the Rails repository please point me to it.

Thanks in advance. :)

Answer

This happens because you're setting the @user instance variable on the instance of RenderService rather than on the controller instance (@obj).

render creates an instance of ActionView::Base (self in a template) and then copies the controller's instance variables to the view instance. Since @user isn't set on the controller instance, it's never copied and no such variable is set in the view. The view calls model_name on the nonexistent @user instance variable and you get the error you saw.

You should be able to fix it by setting @user on the controller instance. Instead of

@user = User.new(user_params)

do

@obj.instance_variable_set :@user, User.new(user_params)

It's difficult to fully describe what happens when a Rails controller renders a view, because there is a lot of subclassing and overridden methods, so if you really want to understand it I recommend stepping through a render call in a debugger. Eventually you'll get to ActionView::Rendering#_render_template and then #view_context. There you'll see the call to #view_assigns, where the controller tells the view what instance variables it has.