1

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

1 Answer 1

2

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.

Sign up to request clarification or add additional context in comments.

3 Comments

where did you learn all of this ? Did you dig into Rails source code?
take a look on the result if you are curios, looks very nice gist.github.com/SuperManEver/cc0eb3698a4e1d8efd38d5e92c00e362
Great! Regarding how to learn these things and where they are, I added to my answer.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.