I am trying to reduce the repetitive code with the following pattern in an ERB template:
<% if content_for(some_key) %>
<%= yield(some_key) %>
<% else %>
Some default values here
<% end %>
I've tried defining the following method in ApplicationHelper but understandably it's not working as expected;
def content_for_with_default(key, &block)
if content_for?(key)
yield(key)
else
block.call
end
end
Here's how I'm trying to use it:
<%= content_for_with_default(some_key) do %>
Some default values here
<% end %>
How can I write the content_for_with_default helper so that it has the intended effect?
Your helper should be like this:
def content_for_with_default(key, &block)
if content_for?(key)
content_for(key)
else
capture(&block)
end
end
EDIT: difference between capture(&block) and block.call
After the erb file is compiled, the block will be some ruby code like this:
');#output_buffer.append= content_for_with_default('some_key') do #output_buffer.safe_concat('
');
#output_buffer.safe_concat(' Some default values here
'); end
You see, the strings within the block are concatenated to the output_buffer and safe_concate returns the whole output_buffer.
As a result, block.call also returns the whole output_buffer. However, capture(&block) creates a new buffer before calling the block and only returns the content of the block.
Related
I have a custom FormBuilder that has a group method that acts as a wrapper around each label/input/hint/error combo. The group method itself is very simple, but when I try to use it from ERB like I would with "fields_for" or similar, it does not render properly.
def group(**options, &)
options[:class] = class_names(options[:class] || "flex flex-col mt-4", options.delete(:classes))
content_tag(:div, capture(&), **options)
end
In the ViewComponent that is using the form helper I can do the following just fine
def call
#form.group(**#group) do
concat #form.label(:tags, #label.delete(:text), **#label)
concat #form.text_field(:tags, **#system_arguments)
end
end
But if I try to write that in an ERB partial, it either does not render the wrapper from group at all, or it only renders the text_field and not the label
<%= #form.group(**#group) do %>
<%= #form.label(:tags, #label.delete(:text), **#label) %>
<%= #form.text_field(:tags, **#system_arguments) %>
<% end %>
Not sure what I'm missing to get the ERB version to work properly...
There is a fix, but it's not released yet:
https://github.com/ViewComponent/view_component/pull/1650
# Gemfile
gem "view_component", github: "ViewComponent/view_component"
# config/application.rb
config.view_component.capture_compatibility_patch_enabled = true
https://viewcomponent.org/known_issues.html#compatibility-with-rails-form-helpers
I'm making a style guide where I output the code on the right that is displayed on the left.
I know that adding %% escapes ERB
I have written a helper that takes the contents of a block and renders the code in two places one showing the html and I want the other to show the source ERB that created the html.
The problem is I get back HTML where I wanted ERB.
The View Code
<%= display_code do %>
<%= link_to "Button", "/style_guide, class: "btn" %>
<% end %>
The Helper Code
module StyleGuideHelper
def display_code(&block)
content = with_output_buffer(&block)
html = ""
html << content_tag(:div, content, class: "rendered-code")
html << content_tag(:div, escape_erb(content), class: "source-code-preview")
html.html_safe
end
def escape_erb(code)
code = code.gsub("%=", "%%=")
end
end
Expected Result
Button <%= link_to "Button", "/style_guide, class: "btn" %>
Actual Result
Button Button
Cheers
The issue is that this helper runs the block (link_to "Button", ...) -- it never sees the source code inside the block, just its output. You could replace escape_erb with h to capture the resulting HTML, but that won't pop back up to the ERB that generated it.
As I see it, your options are:
Break out examples into partials, then make a helper that a) renders the partial and b) displays the underlying file.
Specify your ERB fragments as strings (heredocs?), pass the string into the helper, and have the helper a) evaluate it via ERB.new(string).result(binding) to render the result and b) display the string.
Make the helper determine what part of the view invoked it, then parse the .erb well enough to find the block. Catch is, the precise format of what you see in callers is subject to change without notice due to the way views are compiled.
Make a helper that uses crazy metaprogramming juju to evaluate the block in both an ERB context as well as your own special context that intercepts the code being evaluated and turns it back into markup.
...sorted in approximate order of complexity and odds of success.
This code below will allow you to retrieve the code for a given block.
class ERBSource
ERB = ::ActionView::Template::Handlers::ERB
def self.for(block)
new(block).source
end
attr_reader :block, :file, :line_number
def initialize(block)
#block = block
#file, #line_number = *block.source_location
end
def source
lines = File.readlines(file)
relevant_lines = lines[(line_number - 1)..-1] || []
extract_first_expression(relevant_lines)
end
private
def extract_first_expression(lines)
code = lines.slice[0,1].join # add the first two lines so it has to iterate less
lines.each do |line|
code << line
return code if correct_syntax?(compile_erb(code))
end
raise SyntaxError, "unexpected $end"
end
def correct_syntax?(code)
stderr = $stderr
$stderr.reopen(IO::NULL)
RubyVM::InstructionSequence.compile(code)
$stderr.reopen(stderr)
true
rescue Exception
$stderr.reopen(stderr)
false
end
def compile_erb(code)
ERB.erb_implementation.new(
code,
:escape => false,
:trim => (ERB.erb_trim_mode == "-")
).src
end
end
This is what the helper looks like
module StyleGuideHelper
def render_example(name, &block)
code = ERBSource.for(block)
content_tag(:h2, name) +
content_tag(:div, &block) +
content_tag(:pre, content_tag(:code, code))
end
end
In the Rails 3.2 CaptureHelper, what are the differences between using capture and content_for, and why would I choose one over the other?
If you check out the content_for source, it calls capture internally:
def content_for(name, content = nil, &block)
if content || block_given?
content = capture(&block) if block_given?
#view_flow.append(name, content) if content
nil
else
#view_flow.get(name)
end
end
So, from reading through the method, it looks like the primary advantage of content_for is that it can be called multiple times with multiple blocks for the same named content and each additional call will just append onto whatever has already been rendered. Whereas, in the case of capture, if you call:
<% #greeting = capture do %>
Hello
<% end %>
and then later call:
<% #greeting = capture do %>
Or, in espanol, Hola
<% end %>
Then the last part is the only part that will be captured, and the 'Hello' will just be discarded. Whereas, doing something similar in content_for will result in the second call being appended to 'Hello'.
Hey guys
I'm new to rails. I made this small test code for learning helper in rails:
apps/helpers/home_helper.rb
module HomeHelper
def show(var)
yield var
end
end
apps/views/home/index.html.rb
<%= show('hello world')%>
When I navigate to the url localhost:3000/home/index I got nothing in the html source
What did I do wrong?
There are few things to note here:
module HomeHelper
def show(var)
yield var
end
end
Firstly you're using yield which will pass control to the block given to the method. However, you then call the method without a block:
<%= show('hello world') %>
If you would have had a block it would have looked something like this:
<%= show('hello world') do |v| %>
<%= v %>
<% end %>
This would have output 'hello world' as you expected.
Most like you meant:
module HomeHelper
def show(var)
var
end
end
This returns the value you're passing in and will output it to the response stream.
While block helpers can often to be useful for drying up your code most of the time you want a partial with a layout.
why are you doing yield? Go with simple return:
def show(var)
var
end
Remove the yield. yield is meant for blocks - did you mean return? (it is optional).
Another (arguably better) option is to set the display text in your controller.
home_controller.rb
HomeController < ActionController
# Other controller code...
def index
#text = "Hello, world!"
end
end
index.html.erb
<%= #text %>
How does rails get away with the following in an .erb file?
<%= yield :sidebar %>
<%= yield :other_bar %>
<%= yield :footer %>
How are they able to yield multiple times in the same context to different symbols? Is this some kind of rails magic?
I'm totally familiar with:
def some_method(arg1, arg2, &block)
yield(:block)
end
To my knowledge following doesn't work:
def some_incorrect_method(arg1, &block1, &block2)
yield(:block1)
yield(:block2)
end
So how are they doing it? How do they make it work?
They are passing a symbol into yield...
yield :symbol
...not yielding to a different block.
It works more like this:
def some_method(arg1, arg2, &block)
yield(:some_symbol1)
yield(:some_symbol2)
end
some_method do |symbol|
case symbol
when :some_symbol1
# do A
when :some_symbol2
# do B
else
# unrecognised symbol?
end
end
Do you mean http://apidock.com/rails/ActionView/Helpers/CaptureHelper/content_for ?