39

I'm trying to create an HTML string using a view. I would like to render this from a class that is not a controller. How can I use the rails rendering engine outside a controller? Similar to how ActionMailer does?

Thanks!

prajo
  • 737
  • 2
  • 8
  • 17

9 Answers9

60

Rails 5 and 6 support this in a much more convenient manner that handles creating a request and whatnot behind the scenes:

rendered_string = ApplicationController.render(
  template: 'users/show',
  assigns: { user: @user }
)

This renders app/views/users/show.html.erb and sets the @user instance variable so you don't need to make any changes to your template. It automatically uses the layout specified in ApplicationController (application.html.erb by default). Full documentation can be found here.

The test shows a handful of additional options and approaches.

coreyward
  • 77,547
  • 20
  • 137
  • 166
  • If any one would like to generate text without the controller, look for: http://ruby-doc.org/stdlib-2.4.2/libdoc/erb/rdoc/ERB.html#method-c-new with this, you don't have to create the logic inside the `views` directory. – Eduardo Santana Dec 03 '17 at 14:21
  • 1
    @EduardoSantana That's great if you have a plain ERB template you want to render, but it won't render ActionView views. I.e., you don't get instance variables, partial rendering, helper methods, etc. – coreyward Dec 03 '17 at 16:15
  • 3
    Using controller classes trigged warden and authentication issues. This worked for me: `ActionView::Base.new('app/views').render(file: 'invoices/show', locals: {invoiceObjIn: inv}, layout: 'layouts/invoice')` – Julien Lamarche Aug 08 '18 at 16:23
  • 2
    I have this error: `Devise could not find the 'Warden::Proxy' instance on your request environment.`. How do you solve this? – alagaesia Sep 04 '18 at 11:42
  • 1
    @alagaesia Please open a new question instead of commenting when you have a problem so that others can jump in and help. – coreyward Sep 14 '18 at 18:55
  • Awesome, thank you! In my case the templates are using local variables instead of instance variables, so substituting `locals` for `assigns` worked for me. – joe Jan 30 '19 at 19:48
47

You can use ActionView::Base to achieve this.

view = ActionView::Base.new(ActionController::Base.view_paths, {})
view.render(file: 'template.html.erb')

The ActionView::Base initialize takes:

  1. A context, representing the template search paths
  2. An assigns hash, providing the variables for the template

If you would like to include helpers, you can use class_eval to include them:

view.class_eval do
  include ApplicationHelper
  # any other custom helpers can be included here
end
cweston
  • 11,297
  • 19
  • 82
  • 107
  • 2
    Using ActionView::Base is the best answer, IMO. I'd probably go a small step further and subclass it for convenience, but that's just me. I've never gotten AbstractController to work the way that others describe, but maybe my version of Rails is too recent. Either way, this way feels less dirty. – Ten Bitcomb Feb 04 '16 at 23:50
  • 1
    What about nested layouts ? – Cyril Duchon-Doris Jan 20 '17 at 18:31
  • To render with layout: `view.render(file: 'template.html.erb', layout: 'layouts/layout.html.erb')` – LuizSignorelli Jun 19 '18 at 12:25
10

In Rails 5:

view = ActionView::Base.new(ActionController::Base.view_paths)
view.render(file: 'template.html.erb')

In Rails 6.1:

lookup_context = ActionView::LookupContext.new(ActionController::Base.view_paths)
context = ActionView::Base.with_empty_template_cache.new(lookup_context, {}, nil)

renderer = ActionView::Renderer.new(lookup_context)
renderer.render(context, { file: 'app/views/template.html.erb' })
  • ActionView::Renderer.new() takes a lookup_context arg, and render() method takes a context, so we set those up first
  • ActionView::Base is the default ActiveView context, and must be initialized with with_empty_template_cache method, else render() will error
  • The {}, nil are required assigns and controller args, which used to default to {}, nil in Rails 5
  • Rails 6.1 requires a full filepath file: 'app/views/template.html', whereas Rails 5 only required the filename
DTOWST
  • 116
  • 1
  • 2
  • This comment was hard for me to find, but resolves much of the confusion I was facing trying to update an ActionView::Base call to work in Rails 6.1. If you wouldn't mind, could you add a few more details to the purpose of lookup_context, assigns, and *why* we have to init ActionView::Base with an empty_cache_template now? These are all breaking changes for using ActionMailer to render a PDF and send a message with the PDF as an attachment, which was our implementation since Rails 3. – tgmerritt Jun 10 '22 at 22:25
7

There is no need to over bloat your app with too many gems. As we know ERB is already included in your Rails app.

@jdf = JDF.new
@job = ERB.new(File.read(Rails.root + "app/views/entries/job.xml.erb"))
result =  @job.result(binding)

Above there is snippet of code of an app I'm working on.

  • @jdf is a object to be evaluated in the erb view.
  • In my case I needed to render xml.
  • result is a string to be saved or sent anywhere you like.
Jesse
  • 418
  • 5
  • 8
3

"I'm trying to create an HTML string using a view." -- If you mean you're in the context of a view template, then just use a helper method or render a partial.

If you're in some other "Plain Old Ruby Object", then keep in mind you're free to use the ERB module directly:

erb = ERB.new("path/to/template")
result = erb.result(binding)

The trick is getting that 'binding' object that gives the context for the code in the template. ActionController and other Rails classes expose it for free, but I couldn't find a reference that explains where it comes from.

http://www.ruby-doc.org/stdlib-2.2.0/libdoc/erb/rdoc/ERB.html#method-i-result

vpsz
  • 457
  • 3
  • 6
  • I'm doing this, but there seems to be a problem with using the helpers in my erb file, how would you go about that? https://stackoverflow.com/questions/62211842/rails-how-to-use-helpers-with-erb-new-in-service?noredirect=1#comment110027118_62211842 – Don Giulio Jun 05 '20 at 09:44
3

Calling directly render method on ApplicationController may raise error

 Helper Devise: could not find the `Warden::Proxy` instance on request environment

Instead we can use ApplicationController.renderer.render like

 rendered_string = ApplicationController.renderer.render(
     partial: 'users/show',
     locals: { user: user }
)
Syed Shibli
  • 992
  • 1
  • 12
  • 15
2

Technically, ActionMailer is a subclass implementation of AbstractController::Base. If you want to implement this functionality on your own, you'll likely want to inherit from AbstractController::Base as well.

There is a good blog post here: https://www.amberbit.com/blog/2011/12/27/render-views-and-partials-outside-controllers-in-rails-3/ that explains the steps required.

locriani
  • 4,995
  • 2
  • 23
  • 26
0

For future reference, I ended up finding this handy gem that makes this a breeze:

https://github.com/yappbox/render_anywhere

prajo
  • 737
  • 2
  • 8
  • 17
  • 4
    The referenced library hasn't been updated since 2015 and is incompatible with Rails 4 and 5. – coreyward Oct 15 '17 at 19:35
  • First line from that gem's README.md file (since Nov 08, 2019): DEPRECATED: This gem is no longer needed as of Rails 5. Use ActionController::Renderer instead. – ZedTuX Aug 08 '22 at 08:53
-2

Best to render the view using a controller, as that's what they're for, and then convert it to a string, as that is your ultimate goal.

require 'open-uri'
html = open(url, &:read)
Andrew Grimm
  • 78,473
  • 57
  • 200
  • 338
steven_noble
  • 4,133
  • 10
  • 44
  • 77
  • Please don't post identical answers to multiple questions. Post one good answer, then vote/flag to close the other questions as duplicates. If the question is not a duplicate, *tailor your answers to the question.* – durron597 Sep 18 '15 at 01:59