root/trunk/lib/merb/mixins/render.rb

Revision 1320, 16.3 kB (checked in by v..@exdolo.com, 10 months ago)

allow for explicitly rendering an object, which might otherwise not be renderable (such as a Hash, which would be taken for an options hash)

Line 
1 module Merb
2
3   module RenderMixin
4     @@cached_templates = {}
5     include Merb::ControllerExceptions
6    
7     def self.included(base)
8       base.class_eval {
9         class_inheritable_accessor :_template_root,
10                                    :_layout,
11                                    :_templates,
12                                    :_cached_partials
13                                    
14         self._layout = :application
15         self._template_root = File.expand_path(Merb.view_path)
16         self._templates = {}
17         self._cached_partials = {}
18        
19         attr_accessor :template
20       }
21     end
22
23     # Universal render method. Template handlers are registered
24     # by template extension. So you can use the same render method
25     # for any kind of template that implements an adapter module.
26     #
27     # Out of the box Merb supports Erubis. In addition, Haml, Markaby
28     # and Builder templates are built in, but you must activate them in
29     # merb_init.rb by listing the name of the template engine you
30     # want to use:
31     #
32     #   Merb::Template::Haml
33     #
34     # In addition, you can identify the type of output with an
35     # extension in the middle of the filename. Erubis is capable of
36     # rendering any kind of text output, not just HTML.
37     # This is the recommended usage.
38     #
39     #  index.html.erb update.js.erb feed.xml.erb
40     #
41     # Examples:
42     #
43     #   render
44     #
45     # Looks for views/controllername/actionname.* and renders
46     # the template with the proper engine based on its file extension.
47     #
48     #   render :layout => :none
49     #
50     # Renders the current template with no layout. XMl Builder templates
51     # are exempt from layout by default.
52     #
53     #   render :action => 'foo'
54     #
55     # Renders views/controllername/foo.*
56     #
57     #   render :nothing => 200
58     #
59     # Renders nothing with a status of 200
60     #
61     #   render :template => 'shared/message'
62     #
63     # Renders views/shared/message
64     #
65     #   render :js => "$('some-div').toggle();"
66     #
67     # If the right hand side of :js => is a string then the proper
68     # javascript headers will be set and the string will be returned
69     # verbatim as js.
70     #
71     #   render :js => :spinner
72     #
73     # When the rhs of :js => is a Symbol, it will be used as the
74     # action/template name so: views/controllername/spinner.js.erb
75     # will be rendered as javascript
76     #
77     #   render :js => true
78     #
79     # This will just look for the current controller/action template
80     # with the .js.erb extension and render it as javascript
81     #
82     # XML can be rendered with the same options as Javascript, but it
83     # also accepts the :template option. This allows you to use any
84     # template engine to render XML.
85     #
86     #   render :xml => @posts.to_xml
87     #   render :xml => "<foo><bar>Hi!</bar></foo>"
88     #
89     # This will set the appropriate xml headers and render the rhs
90     # of :xml => as a string. SO you can pass any xml string to this
91     # to be rendered.
92     #
93     #   render :xml => :hello
94     #
95     # Renders the hello.xrb template for the current controller.
96     #
97     #   render :xml => true
98     #   render :xml => true, :action => "buffalo"
99     #
100     # Renders the buffalo.xml.builder or buffalo.xerb template for the current controller.
101     #
102     #   render :xml=>true, :template => 'foo/bar'
103     #
104     # Renders the the foo/bar template. This is not limited to
105     # the default rxml, xerb, or builder templates, but could
106     # just as easy be HAML.
107     #
108     # Render also supports passing in an object
109     # ===Example
110     #
111     #   class People < Application
112     #     provides :xml
113     #
114     #     def index
115     #       @people = User.all
116     #       render @people
117     #     end
118     #   end
119     #   
120     # This will first check to see if a index.xml.* template extists, if not
121     # it will call @people.to_xml (as defined in the add_mime_type method) on the passed
122     # in object if such a method exists for the current content_type 
123     #
124     # Conversely, there may be situations where you prefer to be more literal
125     # such as when you desire to render a Hash, for those occasions, the
126     # the following syntax exists:
127     #   
128     #   class People < Application
129     #     provides :xml
130     #
131     #     def index
132     #       @people = User.all
133     #       render :obj => @people
134     #     end
135     #   end
136     #
137     # When using multiple calls to render in one action, the context of the render is cached for performance reasons
138     # That is, all instance variables are loaded into the view_context object only on the first call and then this is re-used.
139     # What this means is that in the case where you may want to render then set some more instance variables and then call render again
140     # you will want to use a clean context object.  To do this
141     #
142     # render :clean_context => true
143     #
144     # This will ensure that all instance variable are up to date in your views.
145     #
146     def render(*args,&blk)
147       opts = (Hash === args.last) ? args.pop : {}
148    
149       action = opts[:action] || params[:action]
150       opts[:layout] ||= _layout
151    
152       choose_template_format(Merb.available_mime_types, opts)
153      
154       # Handles the case where render is called with an object
155       if obj = args.first || opts[:obj]
156         # Check for a template
157         unless find_template({:action => action}.merge(opts))
158           fmt = content_type
159           if transform_method = Merb.mime_transform_method(fmt)
160             set_response_headers fmt
161             transform_args = provided_format_arguments_for(fmt)
162             return case transform_args
163               when Hash   then obj.send(transform_method, transform_args)
164               when Array  then obj.send(transform_method, *transform_args)
165               when Proc   then
166                 case transform_args.arity
167                   when 3 then transform_args.call(obj, self, transform_method)
168                   when 2 then transform_args.call(obj, self)
169                   when 1 then transform_args.call(obj)
170                   else transform_args.call
171                 end
172               else obj.send(transform_method)
173             end
174           end 
175         end
176       end
177      
178       case
179       when status = opts[:nothing]
180         return render_nothing(status)
181        
182       when opts[:inline]
183         text = opts.delete(:inline)
184         return render_inline(text, opts)
185       else   
186         set_response_headers @_template_format
187        
188         case @_format_value
189         when String
190           return @_format_value
191         when Symbol
192           if !Merb.available_mime_types.keys.include?(@_format_value) # render :js => "Some js value"
193             template = find_template(:action => @_format_value)
194           else
195             if opts[@_format_value] == @_format_value # An edge case that lives in the specs
196                                     # says that a render :js => :js should be catered for
197               template = find_template(:action => @_format_value)
198             else
199               # when called from within an action as plain render within a respond_to block
200               template = find_template(opts.merge( :action => action ))
201             end
202            end
203         else
204           if template = opts[:template]
205             # render :template => "this_template"
206             template = find_template( :template => template)
207           else
208             # a plain action render
209             # def index; render; end
210             template = find_template(:action => action)
211           end
212         end
213       end
214      
215       unless template
216         raise TemplateNotFound, "No template matched at #{unmatched}"
217       end
218       self.template ||= File.basename(template)
219
220       engine = Template.engine_for(template)
221       options = {
222         :file => template,
223         :view_context  => (opts[:clean_context] ? clean_view_context(engine) : cached_view_context(engine)),
224         :opts => opts
225       }
226       content = engine.transform(options)
227       if engine.exempt_from_layout? || opts[:layout] == :none || [:js].include?(@_template_format)
228         content
229       else
230         wrap_layout(content, opts)
231       end
232     end
233    
234     def set_response_headers(tmpl_fmt)
235       if self.respond_to?(:headers)
236         # Set the headers
237         headers['Content-Type'] = Merb.available_mime_types[tmpl_fmt].first
238        
239         # set any additinal headers that may be associated with the current mime type
240         Merb.response_headers[tmpl_fmt].each do |key,value|
241           headers[key.to_s] = value
242         end
243        
244       end
245     end
246
247     def render_inline(text, opts)
248       # Does not yet support format selection in the wrap_layout
249       # Needs to get the template format need a spec for this
250       # should be
251       choose_template_format(Merb.available_mime_types, opts)
252      
253       engine = Template.engine_for_extension(opts[:extension] || 'erb')
254       options = {
255         :text => text,
256         :view_context  => (opts[:clean_context] ? clean_view_context(engine) : cached_view_context(engine)),
257         :opts => opts
258       }
259       content = engine.transform(options)
260       if engine.exempt_from_layout? || opts[:layout] == :none
261         content
262       else
263         wrap_layout(content, opts)
264       end
265     end
266    
267     # does a render with no layout. Also sets the
268     # content type header to text/javascript. Use
269     # this when you want to render a template with
270     # .jerb extension.
271     def render_js(template=nil)
272       render :js => true, :action => (template || params[:action])
273     end
274
275     # renders nothing but sets the status, defaults
276     # to 200. does send one ' ' space char, this is for
277     # safari and flash uploaders to work.
278     def render_nothing(status=200)
279       @_status = status
280       return " "
281     end
282
283         # Sets the response's status to the specified value.  Use either an
284         # integer (200, 201, 302, etc.), or a Symbol as defined in 
285         # Merb::ControllerExceptions::RESPONSE_CODES, such as :not_found,
286         # :created or :see_other.
287     def set_status(status)
288       if status.kind_of?(Symbol)
289         status  = Merb::ControllerExceptions::STATUS_CODES[status]
290         status || raise("Can't find a response code with that name")
291       end
292       @_status = status
293     end
294
295     def render_no_layout(opts={})
296       render opts.update({:layout => :none})
297     end
298    
299     # This is merb's partial render method. You name your
300     # partials _partialname.format.* , and then call it like
301     # partial(:partialname).  If there is no '/' character
302     # in the argument passed in it will look for the partial
303     # in the view directory that corresponds to the current
304     # controller name. If you pass a string with a path in it
305     # you can render partials in other view directories. So
306     # if you create a views/shared directory then you can call
307     # partials that live there like partial('shared/foo')
308     def partial(template, opts={})
309       choose_template_format(Merb.available_mime_types, {}) unless @_template_format
310       template = _cached_partials["#{template}.#{@_template_format}"] ||= find_partial(template)
311       unless template
312         raise TemplateNotFound, "No template matched at #{unmatched}"
313       end
314      
315       opts[:as] ||= template[(template.rindex('/_') + 2)..-1].split('.').first
316
317       if opts[:with] # Render a collection or an object
318        partial_for_collection(template, opts.delete(:with), opts)
319       else # Just render a partial
320        engine = Template.engine_for(template)
321        render_partial(template, engine, opts || {})
322       end
323     end
324
325     # +catch_content+ catches the thrown content from another template
326     # So when you throw_content(:foo) {...} you can catch_content :foo
327     # in another view or the layout.
328     def catch_content(name)
329       thrown_content[name]
330     end
331    
332     private
333
334       def render_partial(template, engine, locals={})
335         @_merb_partial_locals = locals
336         options = {
337           :file => template,
338           :view_context => clean_view_context(engine),
339           :opts => { :locals => locals }
340         }
341         engine.transform(options)
342       end
343
344       def partial_for_collection(template, collection, opts={})
345         # Delete the internal keys, so that everything else is considered
346         # a local declaration in the partial
347         local_name = opts.delete(:as)
348
349         engine = Template.engine_for(template)
350
351         buffer = []
352
353         collection = [collection].flatten
354         collection.each_with_index do |object, count|
355           opts.merge!({
356               local_name.to_sym => object,
357               :count            => count
358           })
359           buffer << render_partial(template, engine, opts)
360         end
361
362         buffer.join
363       end
364    
365       # this returns a ViewContext object populated with all
366       # the instance variables in your controller. This is used
367       # as the view context object for the Erubis templates.
368       def cached_view_context(engine=nil)
369         @_view_context_cache ||= clean_view_context(engine)
370       end
371      
372       def clean_view_context(engine=nil)
373         if engine.nil?
374           ::Merb::ViewContext.new(self)
375         else
376           engine.view_context_klass.new(self)
377         end
378       end
379    
380       def wrap_layout(content, opts={})
381         @_template_format ||= choose_template_format(Merb.available_mime_types, opts)
382        
383         if opts[:layout] != :application
384           layout_choice = find_template(:layout => opts[:layout])
385         else
386           if name = find_template(:layout => self.class.name.snake_case.split('::').join('/'))
387             layout_choice = name
388           else
389             previous_glob = unmatched
390             layout_choice = find_template(:layout => :application)
391           end 
392         end     
393         unless layout_choice
394           raise LayoutNotFound, "No layout matched #{unmatched}#{" or #{previous_glob}" if previous_glob}"
395         end
396
397         thrown_content[:layout] = content
398         engine = Template.engine_for(layout_choice)
399         options = {
400           :file     => layout_choice,
401           :view_context  => cached_view_context,
402           :opts => opts
403         }
404         engine.transform(options)
405       end
406      
407       # OPTIMIZE : combine find_template and find_partial ?
408       def find_template(opts={})
409         if template = opts[:template]
410           path = _template_root / template
411         elsif action = opts[:action]
412           segment = self.class.name.snake_case.split('::').join('/')
413           path = _template_root / segment / action
414         elsif _layout = opts[:layout]
415           path = _template_root / 'layout' / _layout
416         else
417           raise "called find_template without an :action or :layout" 
418         end
419         glob_template(path, opts)
420       end
421      
422       def find_partial(template, opts={})
423         if template =~ /\//
424           t = template.split('/')
425           template = t.pop
426           path = _template_root / t.join('/') / "_#{template}"
427         else 
428           segment = self.class.name.snake_case.split('::').join('/')
429           path = _template_root  / segment / "_#{template}"
430         end
431         glob_template(path, opts)
432       end
433
434       # This method will return a matching template at the specified path, using the
435       # template_name.format.engine convention
436       def glob_template(path, opts = {})
437         the_template = "#{path}.#{opts[:format] || @_template_format}"
438         Merb::AbstractController._template_path_cache[the_template] || (@_merb_unmatched = (the_template + ".*"); nil)
439       end
440      
441       # Chooses the format of the template based on the params hash or the explicit
442       # request of the developer.
443       def choose_template_format(types, opts)
444         opts[:format] ||= content_type
445         @_template_format = (opts.keys & types.keys).first # Check for render :js => etc
446         @_template_format ||= opts[:format]                                         
447         @_format_value = opts[@_template_format] || opts[:format] # get the value of the option if something
448                                               # like :js was used
449                                              
450         # need to change things to symbols so as not to stuff up part controllers
451         if @_template_format.to_s == @_format_value.to_s
452           @_template_format = @_template_format.to_sym
453           @_format_value = @_format_value.to_sym
454         end
455         @_template_format
456       end
457
458       # For the benefit of error handlers, returns the most recent glob
459       # pattern which didn't find a file in the filesystem
460       def unmatched
461         @_merb_unmatched
462       end
463      
464   end 
465 end
Note: See TracBrowser for help on using the browser.