I'm new on RoR and I'm trying to understand how the communication between the view and the layout works.
I found some documentation and I get I need to use provide or content_for methods but it doesn't really explain how it gets accomplished.
Also, why do I need to use yield in my layout to print the value?
Example:
home.html.erb (view)
<% provide(:title, 'Home') %>
application.html.erb (layout)
<title>Great App | <%= yield(:title) %></title>
In your sub-view, you can use either of these methods by passing a block with text content (which can be useful for long content passages such as those that include HTML tags):
<% content_for :title do %>
<h1>The Title</h1>
<% end %>
Or you can simply pass a string directly to #content_view as the second argument:
<% content_for :title, "The Title" %>
There's some better documentation for these methods in the ContentHelper module.
Layouts just wrap other views, and subviews can be thought of as "blocks" that are passed to the layout. If you think of them that way, its natural that the yield keyword is used to invoke the subview like a block.
Related
I'm trying to create a dynamic page title. Is more then just #project.title
The project title, in the page title contains many elements, like the project name, the project category and the project city.
I've tried this
<%= content_for :page_title #project.name, #project.category, #project.city %>
<%= content_for :meta_description, #project.description %>
But I get an error like this one
syntax error, unexpected keyword_ensure, expecting ')'
I've also tried
<%= content_for (:page_title #project.name, #project.category, #project.city) %>
which resulted in the same error: syntax error, unexpected keyword_ensure, expecting ')'
Basically it's just a syntax error. If you want to store a block you use <% content_for :page_title, "My title" %>. Only if you want to reuse this block, you should add a = after the opening tag.
According to the docs you can only pass one content parameter. So you have to do the concatenation by yourself.
<% content_for :page_title, "#{#project.name} #{#project.category} #{#project.city}" %>
The documentation for the content_for helper can be found here.
quick answer
Maybe, just try this in your Project view
<% content_for :page_title, "#{#project.name} #{#project.category} #{#project.city}" %>
tl;dr
content_for stores a bit of code in an identifier. In order to access this stored content later you have to pass the identifier as an argument to content_for.
Note: yield can still be used to retrieve the stored content.
Or in other words, the content_for method allows you to insert content into a named yield block in your layout.
for example
If your current layout contains a yield(:title) ...
<!-- app/views/layouts/application.html.erb -->
<html>
<head>
</head>
<body>
<div id="title"><%= content_for?(:title) ? yield(:title) : "No title block!" %></div>
<div id="main"><%= yield %></div>
</body>
</html>
Then you can insert a title from a view like this
<!-- app/views/projects/index.html.erb -->
<% content_for :head do %>
"#{#project.name} #{#project.category} #{#project.city}"
<% end %>
See: https://api.rubyonrails.org/classes/ActionView/Helpers/CaptureHelper.html#method-i-content_for
and https://rubyplus.com/articles/3081-Layouts-and-Content-For-Tag-in-Rails-5
To answer your question. The error messages you receive tell you that your Ruby code is syntactically incorrect. In idiomatic ruby, it's always wrong to call functions the way you've shown us. Parameters must always be called individually, separated by a comma:
# wrong code, wrong syntax
<%= content_for :page_title #project.name, #project.category, #project.city %>
# wrong code, right *right syntax*
<%= content_for :page_title, #project.name, #project.category, #project.city %>
Also as pointed out in Robin's answer, the docs for content_for show that you can only pass one content parameter. So you have to do the concatenation by yourself.
<% content_for :page_title, "#{#project.name} #{#project.category} #{#project.city}" %>
In my quest to keep my application views as DRY as possible I've encountered a little snag. My appliation.html.erb incorporates a static sidebar menu. Each of my main controllers incorporates a secondary sidebar menu (essentially a submenu). I can take the code that renders the menu out of application.html.erb and put it in each of my views and change the secondary sidebar there, but this produces a lot repetition in my views.
I saw this SO post and looked at this page, but I was unable to get either idea to work. I was thinking that I could put something like:
<% provide(:submenu, 'layouts/sidebars/sidebar_customers_contacts') %>
at the top of each view and use that to render the associated partial by doing
<% content_for(:submenu) do %>
<%= render :partial => :submenu %>
<% end %>
from the application.html.erb but of course that didn't work.
This is my current application.html.erb:
<div class="side">
<%= render 'layouts/sidebar' %>
<%= render 'layouts/sidebars/sidebar_dashboard' %><!-- this needs to load a sidebar based on the controller that calls it. Each view of the controller will get the same sidebar. -->
</div>
<div class="main-content">
<%= yield %>
</div>
I feel like I'm making this more difficult than it really is. Is there a simple way to do this?
Rails provides a helper called controller_name which you can read more about here.
Assuming you adhere to your own naming conventions, this should work as-is. If you decide some controllers don't get a sidebar, you may need to throw in some conditionals...
application.html.erb
<div class="side">
<%= render "layouts/sidebar" %>
<%= render "layouts/sidebars/#{ controller_name }" %>
</div>
<div class="main-content">
<%= yield %>
</div>
EDIT
Sorry, my mistake was using single quotes instead of double-quotes. You cannot use #{string interpolation} within single quotes. Source
So I'm working on an open source project and due to different versions, there's the issue where I can't count on there being a controller for a view. Instead this email would be send out via a rake task for one version and a few others would done via a controller. Now you understand why I'm asking a bad practice question...
I have a layout for a view. Does anyone know a way to specify what the layout is for the view within the view. Some pseudo-code:
<%= extends 'layout/test_mailer` %>
<h1> Hey there! </h1>
And the layout would have the usual yield within it.
I hope I'm explaining the problem good enough.
<%= render partial: "hey_page", layout: "layout/test_mailer" %>
Check part 3.4.3 Partial Layouts at RailsGuides.
I think using yield and content_for should solve the problem. [Guides]
# my_layout.html.erb
<%= yield :mail_view %>
# my_mail_view.html.erb
<%= content_for :mail_view do %>
<!-- html -->
<% end %>
Of-course, if you are using params to get the layout, this would be a wrong answer.
Then, you can also use:
<%= render partial: "link_area", layout: "graybar" %>
You can use
//controller action
def index
render layout: test_mailer
end
//view, index.html.erb
<h1> Hey there! </h1>
//view, layout/test_mail.html.erb
<html>....layout for you test mail
<% yield %>
</html>
I've watched this screencast to add a page title when in a view, is there a way I can do the same but add a class the body tag?
Not sure what you mean, you can do it the same way:
In a view:
<% content_for :body_class, "my_class" %>
In a layout file:
<body class="<%= yield (:body_class) %>">
I usually make a helper method for stuff like this so you can have defaults set up cleanly
application_helper.rb
def body_class(class_name="default_class")
content_for :body_class, class_name
end
view:
<% body_class "foo" %>
application.html.erb
<body class="<%= yield (:body_class) %>">
Sometimes using the current controller name as a class name we'll do:
<body class="<%= controller.controller_name %>">
I find this simpler and a bit more elegant, but of course thus you won't be able to assign individual class names.
s. Add Class To Body Using ERB In A View - Rails
In the layout page:
<% if content_for?(:body_class) %>
<body class="<%= content_for(:body_class) %>" >
<% else %>
<body>
<% end %>
In the content page:
<% content_for :body_class do 'my-body-class' end %>
I've used the accepted method in my app for a while, but never really loved how it worked, because if there is no class, you're gonna have that class=' ' on your body tag, littering your code. For my current use case, I just wanted a widescreen class (but you could easily get more advanced with different classes per your use case). I'm happy with this approach:
In your application helper:
def body_tag(&block)
content = capture(&block)
content_tag(:body, content, class: #widescreen ? "widescreen" : nil)
end
In application.html.erb
<%= body_tag do %>
<%# the rest of your content here %>
<% end %>
Then in your application controller:
private
def enable_widescreen
#widescreen = true
end
Then in any controller that you want it, just do:
before_action :enable_widescreen
Then feel free to make the class logic more advanced if you want to use it for different classes besides 'widescreen' - but the point is that this is an elegant way to allow for there NOT to be a class if you don't specify one, without
<body class>
showing up in your html.
I prefer to use the following method:
<body class="<%= content_for?(:body_class) ? yield(:body_class) : controller_name %>">
That method avoids the dreaded <body class>.
I frequently use the controller name to scope a number of styles so it's nice to not need to supply a content_for on every view if I only needed that one class.
I want to do a conditional rendering at the layout level based on the actual template has defined content_for(:an__area), any idea how to get this done?
#content_for_whatever is deprecated.
Use content_for? instead, like this:
<% if content_for?(:whatever) %>
<div><%= yield(:whatever) %></div>
<% end %>
not really necessary to create a helper method:
<% if #content_for_sidebar %>
<div id="sidebar">
<%= yield :sidebar %>
</div>
<% end %>
then of course in your view:
<% content_for :sidebar do %>
...
<% end %>
I use this all the time to conditionally go between a one column and two column layout
<%if content_for?(:content)%>
<%= yield(:content) %>
<%end%>
Can create a helper:
def content_defined?(var)
content_var_name="#content_for_#{var}"
!instance_variable_get(content_var_name).nil?
end
And use this in your layout:
<% if content_defined?(:an__area) %>
<h1>An area is defined: <%= yield :an__area %></h1>
<% end %>
Ok I am going to shamelessly do a self reply as no one has answered and I have already found the answer :)
Define this as a helper method either in application_helper.rb or anywhere you found convenient.
def content_defined?(symbol)
content_var_name="#content_for_" +
if symbol.kind_of? Symbol
symbol.to_s
elsif symbol.kind_of? String
symbol
else
raise "Parameter symbol must be string or symbol"
end
!instance_variable_get(content_var_name).nil?
end
I'm not sure of the performance implications of calling yield twice, but this will do regardless of the internal implementation of yield (#content_for_xyz is deprecated) and without any extra code or helper methods:
<% if yield :sidebar %>
<div id="sidebar">
<%= yield :sidebar %>
</div>
<% end %>
I use #view_flow and value of the content method before checking if the content is present in the view like this:
#view_flow.content[:header_left_or_whatever_the_name_of_your_block_is].present?
Recently stumbled upon it when showing all local, global and instance variables of self in the console with byebug. I’m a fan using this because it’s straight from Rails, won’t throw an error, won’t hide anything w “Rails magic”, returns a definite true or false, + only checks the content in the current context of the view being rendered.
#view_flow is an instance attribute of ActionView::Context and because Action View contexts are supplied to Action Controller to render a template it will be available to any view that has been rendered by Rails. Although it checks for content, the content_for block will not be yielded if it isn’t there. So it’s been my perfect solution in similar situations.