SuperManEver
SuperManEver

Reputation: 2362

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. :)

Upvotes: 1

Views: 99

Answers (1)

Dave Schweisguth
Dave Schweisguth

Reputation: 37607

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.

Upvotes: 2

Related Questions