When we left off last, we were just about to get into rendering the message. Letâ??s recap that code:
(line 553 action_mailer/base.rb)
def render_message(method_name, body)
if method_name.respond_to?(:content_type)
@current_template_content_type = method_name.content_type
end
render :file => method_name, :body => body
ensure
@current_template_content_type = nil
end
When we call render_message
in create!
, we call it with @template
and @body
, letâ??s see what those get set to inside of render_message
for our example:
@template = method_name => "contact_form"
@body = body => {:message=>"Send this message"}
At the beginning of render_message @body
is set to the hash of instance variables that we set in our contact_form
method in our mailer. Once we are finished processing, that hash will be gone and @body
will be replaced with the full rendered message.
Next, we see that we want to set the @current_template_content_type
instance variable to the proper content type. This is mainly used for multipart so that if render is passed one of those fancy ActionView::ReloadableTemplate
objects it renders it with the proper parser. Since we did not specify a content_type the method_name â??contact_formâ? is passed and we get to skip over that part and go straight to the meat:
render :file => method_name, :body => body
This is not the same render from ActionController
or ActionView
, but it does use the AV template render methods. Letâ??s take a look at the definition:
(line 562 action_mailer/base.rb)
def render(opts)
body = opts.delete(:body)
if opts[:file] && (opts[:file] !~ /\// && !opts[:file].respond_to?(:render))
opts[:file] = "#{mailer_name}/#{opts[:file]}"
end
begin
old_template, @template = @template, initialize_template_class(body)
layout = respond_to?(:pick_layout, true) ? pick_layout(opts) : false
@template.render(opts.merge(:layout => layout))
ensure
@template = old_template
end
end
and the params with our example:
opts => {
:file => "contact_form",
:body => {:message=>"Send this message"}
}
The first thing we do is set the variable body
to opts[:body]
and delete it from the options hash (this is all done in one stroke by the call delete
which returns the value that was deleted). Next, we test to see if opts[:file]
does not match â/â? and does not respond to render then we set opts[:file]
to the same thing but prepended with the mailer_name, in our case âmessage_mailerâ?.
Now we are getting to the real meat. First, we need to set some quick variables before we render. Here are the 2 lines we will be examining next:
old_template, @template = @template, initialize_template_class(body)
layout = respond_to?(:pick_layout, true) ? pick_layout(opts) : false
In the first line we are swapping out the current @template
variable with the result of initialize_template_class(body)
and storing the old value in old_template
. Here is the definition of initialize_template_class
:
(line 603 action_mailer/base.rb)
def initialize_template_class(assigns)
template = ActionView::Base.new(self.class.view_paths, assigns, self)
template.template_format = default_template_format
template
end
and assigns
gets set to:
body => {:message=>"Send this message"}
The first thing we see is that we create a var named template
that is a new instantiation of ActionView::Base
with the view_paths
, assigns
, and the model
(MessageMailer in our example) passed to it. Letâ??s dig in and cross over to ActionView::Base
and look at initialize
:
(line 221 action_pack/action_view/base.rb)
def initialize(view_paths = [], assigns_for_first_render = {}, controller = nil)#:nodoc:
@assigns = assigns_for_first_render
@assigns_added = nil
@controller = controller
@helpers = ProxyModule.new(self)
self.view_paths = view_paths
@_first_render = nil
@_current_render = nil
end
and the params with our example:
view_paths => ["/example_app/app/views"]
assigns_for_first_render => {:message=>"Send this message"}
controller => #<MessageMailer:0x34a579c
@action_name="contact_form",
@subject="Im contacting you",
@content_type="text/plain",
@implicit_parts_order=["text/html", "text/enriched", "text/plain"],
@from="jake@somewhere.com",
@sent_on=Sun Nov 08 11:34:17 -0600 2009,
@headers={},
@default_template_name="contact_form",
@mime_version="1.0",
@body={:message=>"Send this message"},
@mailer_name="message_mailer",
@recipients="someemail@somedomain.com",
@parts=[],
@template="contact_form",
@charset="utf-8">
This allows us to wrap the mailers view around the ActionView
templating system and reuse the rendering module from ActionView
in AM. This also stores our body variables for use inside of our view. Now that we have a template, we need to initialize the template_format
with our mailerâ??s default template format.
template.template_format = default_template_format
Letâ??s open up the definition for default_template_format
(line 577 action_mailer/base.rb)
def default_template_format
if @current_template_content_type
Mime::Type.lookup(@current_template_content_type).to_sym
else
:html
end
end
This method checks the @current_template_content_type
ivar and returns the proper mime-type. This is used as a switch by the renderer. If you dont explicitly set the content type AM defaults to HTML and your message is rendered in HTML mode. Since we didnt define a content_type
in our example @current_template_content_type
is nil and :html will be returned for the default_template_format
.
After that we return the created template instance and that gets us through the initialize_template_class
method.
So now that we have set the @template
ivar in:
old_template, @template = @template, initialize_template_class(body)
layout = respond_to?(:pick_layout, true) ? pick_layout(opts) : false
We move down to setting the layout for the mailer. In Rails you have the ability to use layouts just like in regular views. In our case, if we wanted to use a layout, we would have to add a file named message_mailer.html.erb
in app/views/layouts
. The other option is specifying a layout name in our mailer model (just like we would in a controller). So we could define MessageMailer as:
class MessageMailer < ActionMailer::Base
layout "my_email_layout"
def contact_form(msg_from, message)
...
end
end
In that case, we would need a file named my_email_layout.html.erb
in our layouts directory. Since we include ActionController::Layout
in AM::Base our model will respond to pick_layout
. This is the method that will determine the layout needed for the renderer. Letâ??s dive into pick_layout
, which is defined in ActionController::Layout
# (line 229 action_pack/action_controller/layout.rb)
def pick_layout(options)
if options.has_key?(:layout)
case layout = options.delete(:layout)
when FalseClass
nil
when NilClass, TrueClass
active_layout if action_has_layout? && candidate_for_layout?(:template => default_template_name)
else
active_layout(layout, :html_fallback => true)
end
else
active_layout if action_has_layout? && candidate_for_layout?(options)
end
end
and the params for our example:
options => {:file=>"message_mailer/contact_form"}
The first part of pick_layout
checks for the key :layout
in options and since we didnt define it in our example we will skip over it and straight to the else part of the conditional:
active_layout if action_has_layout? && candidate_for_layout?(options)
Here, we call the method active_layout
if action_has_layout? returns true and candidate_for_layout?(options) returns true. action_has_layout?
is a simple method to check if this action or mailer method has a layout and is basically making sure that your :except and :only actions render properly. Letâ??s take a look at action_has_layout? with our example
# (line 244 action_pack/action_controller/layout.rb)
def action_has_layout?
if conditions = self.class.layout_conditions
case
when only = conditions[:only]
only.include?(action_name)
when except = conditions[:except]
!except.include?(action_name)
else
true
end
else
true
end
end
and conditions
would be:
conditions => nil
Since we are not setting a layout for our message the method self.class.layout_conditions
returns nil. layout_conditions
is a class method on AM that reads the attribute accessor hash layout_conditions
. These conditions are written when you make the call:
layout "my_template" # or layout "my_template", :except => "some_message"
This writes all your conditions to a attribute hash for easy reading and subclassing. Since our example returns nil for layout_conditions
we end up at the outer else statement and return true out of this method and on to candidate_for_layout?(options)
. This is a base method is written in layout.rb and overridden in AM::base.rb:
# (line 585 action_mailer/base.rb)
def candidate_for_layout?(options)
!self.view_paths.find_template(default_template_name, default_template_format).exempt_from_layout?
rescue ActionView::MissingTemplate
return true
end
and our paramaters and extra variables/methods :
options => {:file=>"message_mailer/contact_form"}
default_template_name => "contact_form"
default_template_format => :html
In this method, we are checking to see if there is a template with the above params. Since we dont have any layouts Rails throws an ActionView::MissingTemplate
exception which we catch and just return true from it. This sets up the final part of pick_layout
which is active_layout
. Here is the definition for that:
# (line 201 action_pack/action_controller/layout.rb)
def active_layout(passed_layout = nil, options = {})
layout = passed_layout || default_layout
return layout if layout.respond_to?(:render)
active_layout = case layout
when Symbol then __send__(layout)
when Proc then layout.call(self)
else layout
end
find_layout(active_layout, default_template_format, options[:html_fallback]) if active_layout
end
In our case, we dont pass anything to active_layout so the first line will get set to default_layout
, which is a private method that looks like:
# (line 220 action_pack/action_controller/layout.rb)
def default_layout #:nodoc:
layout = self.class.read_inheritable_attribute(:layout)
return layout unless self.class.read_inheritable_attribute(:auto_layout)
find_layout(layout, default_template_format)
rescue ActionView::MissingTemplate
nil
end
Since we didnt set a layout, the first line will return nil
and the second line will return that value because auto_layout
will also be nil. When we get back to active_layout
, the variable layout
is set to nil
. The variable active_layout
gets set to nil
as well since we are layout-less. Now we get to find_layout
which doesnt get called in our example because active_layout
is nil
. Thatâs it, we are now ready to do the main rendering. Letâs look at the code again:
(line 562 action_mailer/base.rb)
def render(opts)
...
begin
old_template, @template = @template, initialize_template_class(body)
layout = respond_to?(:pick_layout, true) ? pick_layout(opts) : false
@template.render(opts.merge(:layout => layout))
ensure
@template = old_template
end
end
and the params and current variables with our example:
opts => {
:file => "contact_form",
:body => {:message=>"Send this message"}
}
old_template => "contact_form"
@template => #<ActionView::Base:0x3496c10
@template_format=:html,
@assigns={:message=>"Send this message"},
@helpers=#<ActionView::Base::ProxyModule:0x3496b84>,
@controller=#<MessageMailer:0x34a5814
@action_name="contact_form",
@subject="Im contacting you",
@content_type="text/plain",
@implicit_parts_order=["text/html", "text/enriched", "text/plain"],
@from="jake@somewhere.com",
@sent_on=Sun Nov 08 18:15:42 -0600 2009,
@headers={},
@default_template_name="contact_form",
@mime_version="1.0",
@body={:message=>"Send this message"},
@mailer_name="message_mailer",
@recipients="someemail@somedomain.com",
@parts=[],
@template=#<ActionView::Base:0x3496c10 ...>,
@charset="utf-8">,
@_current_render=nil,
@assigns_added=nil,
@_first_render=nil,
@view_paths=["/example_app/app/views"]>
So now we see that layout
ends up being nil in our example which we then pass on to @template.render
along with our original options. Now weâ??ll jump into render
:
# (line 201 action_view/base.rb)
def render(options = {}, local_assigns = {}, &block) #:nodoc:
local_assigns ||= {}
case options
when Hash
options = options.reverse_merge(:locals => {})
if options[:layout]
_render_with_layout(options, local_assigns, &block)
elsif options[:file]
template = self.view_paths.find_template(options[:file], template_format)
template.render_template(self, options[:locals])
elsif options[:partial]
render_partial(options)
elsif options[:inline]
InlineTemplate.new(options[:inline], options[:type]).render(self, options[:locals])
elsif options[:text]
options[:text]
end
when :update
update_page(&block)
else
render_partial(:partial => options, :locals => local_assigns)
end
end
and our passed in params:
options => {:file=>"message_mailer/contact_form", :layout=>nil}
local_assigns => {}
Since options
is a Hash, we jump right in and first merge in :locals => {}
with options
. options[:layout]
returns nil so we skip that statement and go into the options[:file]
conditional
template = self.view_paths.find_template(options[:file], template_format)
template.render_template(self, options[:locals])
Here, we search out the template from the AM models view_paths
with our file and the temlpate_format
, which in our case is :html
. view_paths
is an instance method in ActionView::Base
that accesses the attribute @view_paths
which holds an instance of @ ActionView::PathSet@. This is a helper class that wraps an array with some pretty handy methods, one of which is find_template
. This helper class stores each of the view_paths as a ActionView::ReloadableTemplate::ReloadablePath
object. This allows us to do other cooler things which we go over later. In our case, view_paths
returns:
self.view_paths => ["/example_app/app/views"]
Letâ??s dive into find_template
:
# (line 201 action_pack/action_view/paths.rb)
def find_template(original_template_path, format = nil, html_fallback = true)
return original_template_path if original_template_path.respond_to?(:render)
template_path = original_template_path.sub(/^\//, '')
each do |load_path|
if format && (template = load_path["#{template_path}.#{I18n.locale}.#{format}"])
return template
elsif format && (template = load_path["#{template_path}.#{format}"])
return template
elsif template = load_path["#{template_path}.#{I18n.locale}"]
return template
elsif template = load_path[template_path]
return template
# Try to find html version if the format is javascript
elsif format == :js && html_fallback && template = load_path["#{template_path}.#{I18n.locale}.html"]
return template
elsif format == :js && html_fallback && template = load_path["#{template_path}.html"]
return template
end
end
return Template.new(original_template_path, original_template_path =~ /\A\// ? "" : ".") if File.file?(original_template_path)
raise MissingTemplate.new(self, original_template_path, format)
end
and our params:
original_template_path => "message_mailer/contact_form"
format => :html
The first line just returns back if we have a full fledged ActionView model that responds to render. Since we dont, we keep going and see that the next line makes sure that our original_template_path
isnt an absolute path by removing the initial â??/â? if it is there and storing the final result in template_path
. Next, we iterate over ourselves and do some checking (remember, PathSet is a subclass of Array, so we can call each
and iterate over the contents internally). In our example, we end up executing the line:
elsif template = load_path[template_path]
return template
This is part of the fanciness of ActionView::ReloadableTemplate::ReloadablePath
. One of the methods implemented is []
which works like a hash and pulls out the proper ReloadableTemplate
from the paths array, in our case itâ??s
template => #<ActionView::ReloadableTemplate:0x34a2ad8
@base_path="message_mailer",
@template_path="message_mailer/contact_form.erb",
@_memoized_path=["message_mailer/contact_form.erb"],
@name="contact_form",
@previously_last_modified=Sat Nov 07 10:32:50 -0600 2009,
@_memoized_method_segment=["app47views47message_mailer47contact_form46erb"],
@format=nil,
@filename="/example_app/app/views/message_mailer/contact_form.erb",
@_memoized_relative_path=["app/views/message_mailer/contact_form.erb"],
@extension="erb",
@locale=nil,
@_memoized_path_without_extension=["message_mailer/contact_form"],
@_memoized_method_name_without_locals=["_run_erb_app47views47message_mailer47contact_form46erb"],
@load_path="/example_app/app/views">
So we found the proper view and we are now done with @find_template@. Back to @render@:
# (line 201 action_pack/action_view/base.rb)
def render(options = {}, local_assigns = {}, &block) #:nodoc:
local_assigns ||= {}
case options
when Hash
...
elsif options[:file]
template = self.view_paths.find_template(options[:file], template_format)
template.render_template(self, options[:locals])
elsif options[:partial]
...
end
when :update
...
end
end
The next line renders the template with any locals (in our case there are none). Before we dive into render_template
âs definition, letâs take a look at the view we created for our example message:
(message_mailer/contact_form.erb)
Hi,
I am contacting you with this message:
<%= @message %>
Thanks
Jake
A ridiculously simple mailer view for our equally ridiculously simple mailer message. Back to render_template
# (line 193 action_pack/action_view/template.rb)
def render_template(view, local_assigns = {})
render(view, local_assigns)
rescue Exception => e
raise e unless filename
if TemplateError === e
e.sub_template_of(self)
raise e
else
raise TemplateError.new(self, view.assigns, e)
end
end
and our current params:
view => #<ActionView::Base:0x3496c10
@template_format=:html,
@assigns={:message=>"Send this message"},
@helpers=#<ActionView::Base::ProxyModule:0x3496b84>,
@controller=#<MessageMailer:0x34a5814
@action_name="contact_form",
@subject="Im contacting you",
@content_type="text/plain",
@implicit_parts_order=["text/html", "text/enriched", "text/plain"],
@from="jake@somewhere.com",
@sent_on=Mon Nov 09 20:09:58 -0600 2009,
@headers={},
@default_template_name="contact_form",
@mime_version="1.0",
@body={:message=>"Send this message"},
@mailer_name="message_mailer",
@recipients="someemail@somedomain.com",
@parts=[],
@template=#<ActionView::Base:0x3496c10 ...>,
@charset="utf-8">,
@_current_render=nil,
@assigns_added=nil,
@_first_render=nil,
@view_paths=["/example_app/app/views"]>
local_assigns => {}
As we can see, render_template
is a relatively simple method that delegates most of itâs work to Renderable#render
. Thatâs where we are going next:
# (line 193 action_pack/action_view/renderable.rb)
def render(view, local_assigns = {})
compile(local_assigns)
view.with_template self do
view.send(:_evaluate_assigns_and_ivars)
view.send(:_set_controller_content_type, mime_type) if respond_to?(:mime_type)
view.send(method_name(local_assigns), local_assigns) do |*names|
ivar = :@_proc_for_layout
if !view.instance_variable_defined?(:"@content_for_#{names.first}") && view.instance_variable_defined?(ivar) && (proc = view.instance_variable_get(ivar))
view.capture(*names, &proc)
elsif view.instance_variable_defined?(ivar = :"@content_for_#{names.first || :layout}")
view.instance_variable_get(ivar)
end
end
end
end
Whew, thatâs it for this week. Next week, we get super deep into rendering and see how Rails performs all of itâs rendering duties.