How can I use views and layouts with Ruby and ERB (not Rails)? - ruby

How can I use views and layouts with Ruby and ERB (not Rails)?
Today i'm using this code to render my view:
def render(template_path, context = self)
template = File.read(template_path)
ERB.new(template).result(context.get_binding)
end
This works very well, but how can I implement the same function, but to render the template inside a layout? I want to call render_with_layout(template_path, context = self), and so that it will have a default layout.

Since you tagged it with Sinatra I assume that you us Sinatra.
By default you view is rendered in your default layout called layout.erb
get "/" do
erb :index
end
This renders your view index with the default layout.
If you need multiple layouts you can specify them.
get "/foo" do
erb :index, :layout => :nameofyourlayoutfile
end
* If you don't use Sinatra you may want to borrow the code from there.

If you use the Tilt gem (which I think is what Sinatra uses) you could do something like
template_layout = Tilt::ERBTemplate.new(layout)
template_layout.render {
Tilt::ERBTemplate.new(template).render(context.get_binding)
}

If you are using Sinatra so it has a good docimentation and one of the topics it's nested layouts (see Sinatra README)
Also good idea to use special default layout file (layout.haml or layout.erb in your view directory) This file will be always use to render others. This is example for layout.haml:
!!!5
%html
%head
##<LOADING CSS AND JS, TILE, DESC., KEYWORDS>
%body
=yield ## THE OTHER LAYOUTS WILL BE DISPALYED HERE
%footer
# FOOTER CONTENT

Thanks for all the answers!
I solved it finally by doing this, I hope someone else also can find this code useful:
def render_with_layout(template_path, context = self)
template = File.read(template_path)
render_layout do
ERB.new(template).result(context.get_binding)
end
end
def render_layout
layout = File.read('views/layouts/app.html.erb')
ERB.new(layout).result(binding)
end
And I call it like this:
def index
#books = Book.all
body = render_with_layout('views/books/index.html.erb')
[200, {}, [body]]
end
Then it will render my view, with the hardcoded (so far) layout..

Related

Ruby ERB - Create a content_for method

I'm currently working on an ERB View class for a gem. With this class I would like to have some helper methods for ERB templates.
It's okay about basic helpers like h(string). I found erbh gem who help me to understand more how context works.
But now I'm trying to create a content_for method like there is in Rails or Sinatra.
On first time I was using simple Proc to capture the view block and then just calling call method to print it. It was working enough at the beginning.
But after having completed views I saw wired thinks, some content are printed multiple times.
So I take a look on the Sinatra ContentFor helper to understand how they did it and I copy some methods of this helper. I have no errors, but the block return are always empty... and I don't really know why.
My knowledge about ERB are not good enough to know how ERB buffering works.
Code
Here a gist who explain the status of my code. I extracted the code from my library and simplified it a bit.
https://gist.github.com/nicolas-brousse/ac7f5454a1a45bae30c52dae826d587f/66cf76c97c35a02fc6bf4a3bc13d8d3b587356de
What I would like?
I just would like to have content_for methods works like they do with Rails and Sinatra.
Thanks!
After reading this blog article I finally found why it wasn't working. I don't know if I did it in the best way and cleaner way but it works.
So the bug was mainly from the ERB initilization. By using a property instead a local variable as eoutvar it now works.
erb = ERB.new(str, nil, "<>", "#_erbout")
I also change a bit the capture method who is used by content_for helper.
It looks like this now (gist)
def content_for(key, content = nil, &block)
block ||= proc { |*| content }
content_blocks[key.to_sym] << capture_later(&block)
end
def content_for?(key)
content_blocks[key.to_sym].any?
end
def yield_content(key, default = nil)
return default if content_blocks[key.to_sym].empty?
content_blocks[key.to_sym].map { |b| capture(&b) }.join
end
def capture(&block)
#capture = nil
#_erbout, _buf_was = '', #_erbout
result = yield
#_erbout = _buf_was
result.strip.empty? && #capture ? #capture : result
end
def capture_later(&block)
proc { |*| #capture = capture(&block) }
end

Sinatra method to set layout

I want to write a method in sinatra to set the layout, something like
def admin_layout
set :layout, 'admin/layout'
end
I know I can do things like set :erb, layout: :'main/layout' or specify the layout for each action like
get 'admin/login' do
erb :'admin/login', layout: :'admin/layout'
end
but I'm wondering if there is a way to just abstract that into a method so I don't need to set the layout for each route. I am making an app with a main site and then an admin site, but the admin part is very lightweight, just logging in and being able to edit posts so I am not trying to get too crazy but my current file structure is like this:
db/
models/
public/
views/
admin/
main/
app.rb
config.ru
Define a layout argument:
def admin_layout
{:layout, 'admin/layout'}
end
Then you can use this method as a param like
get 'admin/login' do
## do ......
erb :'admin/login', admin_layout
end
Or if you want to judge which layout it will use, change the admin_layout function like:
def admin_layout
if request.path.start_with?('/admin')
{:layout, 'admin/layout'}
else
{:layout, 'layout'}
end
end
I know I can do things like: set :erb, layout: :'main/layout'
No you can't do that--or at least erb will ignore it because it's not an erb specific option. If you could do that, wouldn't that do what you are asking for here:
but I'm wondering if there is a way to just abstract that into a
method so I don't need to set the layout for each route.
The method already exists--template():
#Templates:
application_layout = <<END_OF_HAML
%html
%head
%title My App
%body
=yield
END_OF_HAML
template :layout do #Creates a template named :layout
application_layout
end
template :page1 do
'%div.greet Hello World!' #=> <div class="greet">Hello World!</div>
end
template :page2 do
'%div#first_name John' #=> <div id="first_name">John</div>
end
#Routes: -------------------
get '/page1' do
haml :page1 #If there is a template named :layout, then the :page1 template will be inserted into the :layout template automatically.
end
get '/page2' do
haml :page2 #If there is a template named :layout, then the :page1 template will be inserted into the :layout template automatically.
end
Then you can override the default layout, like this:
admin_layout = <<END_OF_HAML
%html
%head
%title Admin Only
%body
=yield
END_OF_HAML
template :special_layout do #Creates a template named :special_layout
admin_layout
end
get '/page3' do
haml :special_layout, :layout => false do #Don't use default layout, i.e the :layout template, instead use :special_layout template
haml :page3
end
end
See "Named Templates" here: http://www.sinatrarb.com/intro.html#Named%20Templates

HAML Pass partial dynamically to layout in Sinatra app

I am trying to build a Sinatra app with HAML and use a layout to be able to break my site down into partials:
layout.haml
!!!
%html
%head
= partial :head
%body
= partial :header
= partial :#{#template}
= partial :footer
Where my Sinatra app is calling layout.haml like so:
get '/test' do
#template = "\"test/index\""
haml :"layout"
end
To try to pull in:
views/
|----test/
|----_index.haml
Which gives me the error:
wrong number of arguments (0 for 1..2)
I have also tried redefining a number of different combinations with no success:
#template = ":\"test/index\""
= partial #{#template}
#template = "test/index"
= partial :#{#template}
Note: I am using Sinatra Partials
Am I going about this entirely the wrong way? My brain is really suffering trying to figure out how to employ HAML for such a basic concept of DRY templating.
Ah it was so simple afterall.. With the help of this question, I realized now of course that everything is wrapped with layout.haml and I simply needed to place the appopriate yield statement.
layout.haml
!!!
%html
%head
= partial :head
%body
= partial :header
= yield
= partial :footer
And call the template as usual:
get '/test' do
haml :"test/index"
end

External Haml Variable

I created a layout with a variable inside.
layout.haml:
- title = "Example"
%title #{title}
It works perfect and gave me this:
<title>Example</title>
But, if I put this variable in a partial, it doesn't work
_vars.haml:
- title = "Example"
layout.haml:
=partial "vars"
%title #{title}
How can I define all the variables on an external document and make it work?
Thanks for the help
You are probably looking for content for:
layout.html.haml:
%title= yield(:title)
_my_partial.html.haml:
- content_for(:title) do
Example
Maybe you could put your shared code in helper?
# application_helper.rb
def title
#title ||= 'Example'
end
After that title helper could be used either in primary view or in partial. Notice that calculation of variable will be performed only once due to ||=.

Call Sinatra erb from another class

I need to render a Sinatra erb template inside a class in my controller. I'm having issues calling this though. I've looked in the Sinatra rdocs and have come up with this:
Sinatra::Templates.erb :template_to_render
When I do this, I get the following error:
undefined method `erb' for Sinatra::Templates:Module
Is there a way to call this from another class?
To imitate rendering behavior of Sinatra controller in some other class (not controller) you can create module like this:
module ErbRender
include Sinatra::Templates
include Sinatra::Helpers
include Sinatra::ContentFor
def settings
#settings ||= begin
settings = Sinatra::Application.settings
settings.root = "#{ROOT}/app"
settings
end
end
def template_cache
#template_cache ||= Tilt::Cache.new
end
end
Here you may need to tune settings.root
Usage example:
class ArticleIndexingPostBody
include ErbRender
def get_body
erb :'amp/articles/show', layout: :'amp/layout'
end
end
This will properly render templates with layouts including content_for
why you don't require 'erb' and after use only erb
## You'll need to require erb in your app
require 'erb'
get '/' do
erb :index
end
You could have your class return the template name and render it in the main app.
Of course that's not exactly an answer (I don't have enough rep to add a comment with this account) and you're probably doing just that by now anyway...

Resources