Cannot capture output of block in Rails helper - ruby-on-rails

I've run into a problem with a custom Rails FormBuilder, which drives me crazy since yesterday evening. Basically I want to have an optional block to one of my builder methods, so that I can show additional content within my main content_tag:
def form_field(method, &block)
content_tag(:div, class: 'field') do
concat label(method, "Label #{method}")
concat text_field(method)
capture(&block) if block_given?
end
end
When I call that method in one of my Slim templates, like so:
= f.form_field :email do
small.help-text
| Your email will never be public.
it inserts the block (here: the help text within <small>) above the actual output of the content_tag:
<small class="help-text">Your email will never be public.</small>
<div class="field">
<label for="user_email">Label email</label>
<input type="text" value="" name="user[email]" id="user_email">
</div>
I tried several other variants, but it seems that I can never capture the output of the block. Any ideas - and maybe even more interesting: explanations on this behaviour? I read several articles about that topic and also had a look at the Rails source, but couldn't really figure out why it's behaving like that.

As #Kitto says, :capture, :concat and many more others helpers are implemented to #template.
In my customs FormBuilder, i have this :
module CustomFormBuilder < ActionView::Helpers::FormBuilder
delegate :content_tag, :concat, to: :#template
[ ... your code ... ]
end

So, after some more digging-around, it turns out that the problem lies in the FormBuilder and how itself deals with output buffers. Looking into the source code for ActionView FormHelpers, gives a hint to call capture on #template, like so:
def form_field(method, &block)
content = #template.capture(&block) if block_given?
content_tag(:div, class: 'field') do
concat label(method, "Label #{method}")
concat text_field(method)
concat content if content.present?
end
end

Related

Does Rails' #link_to only accept one parameter in its block when used outside of ERB?

When I did this in ERB, it works as expected, giving me an a tag that wraps around an image and some text
<%= link_to(...) do %>
<img src="..." />
text
<% end %>
But when I tried to put this in a method, the a tag only wraps the last argument, which in this case, is the text.
def build_link
link_to(...) do
image_tag(...)
text
end
end
Looking at the docs, they only gave an example of using link_to in ERB, so is it smart to assume that using it in a method doesn't work as well and can't accept two parameters?
Following up to my comment:
The reason is behavior happens is because of how Ruby handles blocks, and how Rails handles the output for ActionController.
The trick here is to use handy-dandy concat.
def build_link
link_to("#") do
concat image_tag("http://placehold.it/300x300")
concat "hello world"
end
end
Pretend the block you pass to link_to is just another method, and it gets returned some object/value. In this case, your text object gets returned.
But because you want to output both image_tag and text, you need to pass that together to the output.

How to extend a core Rails FormBuilder field

I am using Bootstrap 3 with Rails 4, and I wanted to create a custom FormBuilder to handle some of Bootstrap's unique HTML syntax. Specifically, I needed a custom helper that would create the form-group div wrapper around a form field, since Bootstrap applies error state to this wrapper, and not the field itself...
<div class="form-group has-error">
<label class="col-md-3 control-label" for="user_email">Email</label>
<div class='col-md-9'>
<input class="form-control required-input" id="user_email" name="user[email]" placeholder="peter#example.com" type="email" value="someone#example.com" />
</div>
</div>
Note the extra class has-error in the outer div...
Anyway, I wrote that helper, and it works great!
def form_group(method, options={})
class_def = 'form-group'
class_def << ' has-error' unless #object.errors[method].blank?
class_def << " #{options[:class]}" if options[:class].present?
options[:class] = class_def
#template.content_tag(:div, options) { yield }
end
# Here's a HAML sample...
= f.form_group :email do
= f.label :email, nil, class: 'col-md-3 control-label'
.col-md-9
= f.email_field :email, class: 'form-control required-input', placeholder: t('sample.email')
Now I want to utilize Bootstrap's form help text in order to display error messages. This requires me to extend Rails native helpers (such as text_field in the example above) and then call them within the the block of f.form_group.
The solution seemed simple enough: call the parent, and append my span block onto the end...
def text_field(method, options={})
#template.text_field(method, options)
if !#object.errors[method].blank?
#template.content_tag(:span, #object.errors.full_messages_for(method), class: 'help-block')
end
end
Only it wouldn't output any HTML, the div would simply show up empty. I've tried a bunch of diff syntax approaches:
super vs text_field vs text_field_tag
concat-ing the results -- #template.concat(#template.content_tag( [...] ))
dynamic vars, e.g. def text_field(method, *args) and then options = args.extract_options!.symbolize_keys!
I only ever get weird syntax errors, or an empty div. In some instances, the input field would appear, but the help text span wouldn't, or vice verse.
I'm sure I'm screwing up something simple, I just don't see it.
Took a few days, but I ultimately stumbled onto the proper syntax. Hopefully it saves someone else's sanity!
Ruby's return automagic, combined with Rails at-times complex scoping, had me off kilter. Specifically, #template.text_field draws the content, but it must be returned by the helper method in order to appear inside the calling block. However we have to return the results of two calls...
def text_field(method, options={})
field_errors = object.errors[method].join(', ') if !#object.errors[method].blank?
content = super
content << (#template.content_tag(:span, #object.errors.full_messages_for(method), class: 'help-block') if field_errors)
return content
end
We must return the results of both the parent method (via super) plus our custom #template.content_tag(:span, injection. We can shorten this up a bit using Ruby's plus + operator, which concatenates return results.
def text_field(method, options={})
field_errors = object.errors[method].join(', ') if !#object.errors[method].blank?
super + (#template.content_tag(:span, #object.errors.full_messages_for(method), class: 'help-block') if field_errors)
end
Note: the form was initiated with an ActiveModel object, which is why we have access to #object. Implementing form_for without associating it with a model would require you to extend text_field_tag instead.
Here's my completed custom FormBuilder
class BootstrapFormBuilder < ActionView::Helpers::FormBuilder
def form_group(method, options={})
class_def = 'form-group'
class_def << ' has-error' unless #object.errors[method].blank?
class_def << " #{options[:class]}" if options[:class].present?
options[:class] = class_def
#template.content_tag(:div, options) { yield }
end
def text_field(method, options={})
field_errors = object.errors[method].join(', ') if !#object.errors[method].blank?
super + (#template.content_tag(:span, #object.errors.full_messages_for(method), class: 'help-block') if field_errors)
end
end
Don't forget to tell form_for!
form_for(:user, :builder => BootstrapFormBuilder [, ...])
Edit: Here's a number of useful links that helped me along the road to enlightenment. Link-juice kudos to the authors!
Writing a custom FormBuilder in Rails 4.0.x
Formatting Rails Errors for Twitter Bootstrap
Very Custom Form Builders in Rails
SO: Nesting content tags in rails
SO: Rails nested content_tag
SO: Rails 3 Custom FormBuilder Parameters
SO: Trying to extend ActionView::Helpers::FormBuilder
RailsGuides: Form Helpers
Real world sample from treebook tutorial code

method attributes [ajax,jquery,rails4]

I am reading the book Agile web developpment with rails 4.
there is a part where the products' cart is showing only if it is not empty, my question is the function in the view send to the helper only 2 attributes while in the implementation there are 3 parameters.
in the view I have the bellow code, which render to _cart where I have the cart show
<%= hidden_div_if(#cart.line_items.empty?, id: 'cart') do %>
<%= render #cart %>
<% end %>
the helper has:
module ApplicationHelper
def hidden_div_if(condition, attributes = {}, &block)
if condition
attributes["style"] = "display: none"
end
content_tag("div", attributes, &block) end
end
My question is the &block in this case receives id: 'cart' but is it a optional attibute? that why it comes with &. but what about attributes = {}?
I am really not sure how that is happening, could someone explain me a bit?
Thanks!!
The code between and including do and end is the block, and this is the third argument for hidden_div_if, which is simply passed on to content_tag. The & in the definition of hidden_div_if captures the block in your view, whereas the & in the call to content_tag expands it again to pass it along.
The answer here explains this idea nicely with a few examples. I recommend testing everything out yourself in irb to get a feel for it.

Ruby and Rails: Statement Modifiers in Views?

I have this code
<% if approved %>
<td>Flow Number</td>
<% end %>
and I'd like to shorten it using statement modifiers. Of course I can use
<%="<td>Flow Number</td>" if approved -%>
but is there a shorter way? I'd also like to get the markup out of quotes.
You could use "content_tag", which isn't actually shorter, but may be more appealing, keeping HTML out of your ruby blocks:
<%= content_tag :td, "Flow Number" if approved %>
Otherwise, you could consider writing a helper - which may be appealing if you need to reuse similar logic throughout the page (or over several pages).
Maybe HAML?
That'd be:
- if approved?
%td Flow Number
Not exactly what you're after I know.
Yeah, I think a helper method using content_tag internally would be the best short way.
Using a helper method, you could also yield to the desired output like this:
# in view helper
def show_if(condition, wrapper_tag)
condition ? content_tag(wrapper_tag, yield) : ''
end
# in view
<%= show_if(approved, :td) {'Flow Number'} %>
or
# in view helper
def show_if(condition)
condition ? yield : ''
end
# in view
<% show_if(approved) do %>
<td>Flow Number</td>
<% end %>
I like this last method for a nice generic way to show or hide whole blocks based on a condition. Hope that helps!

Nesting content tags in rails

Hey. I've got some code from Agile Web Development that wraps some HTML around a method call as so:
# from tagged_builder.rb, included as a FormBuilder helper
def self.create_tagged_field(method_name)
define_method(method_name) do |label, *args|
#template.content_tag("p",
#template.content_tag("label" ,
label.to_s.humanize.capitalize,
:for => "#{#object_name}_#{label}")
+
super)
end
end
I would like to nest a span tag within the label content_tag, so that the final output would be along the lines of:
<p><label>Name
<span class="small">Add your name</span>
</label>
<input type="text" name="textfield" id="textfield" />
I am wondering how I go about including the span's content (say a variable such as 'warning')
I have tried all sorts, to no avail. The methods call ok (such as f.text_field :name will produce
<p><label for="object_name">Name</label></p>
Have tried this:
def self.create_tagged_field(method_name)
define_method(method_name) do |label, warning, *args|
#template.content_tag("p",
#template.content_tag("label" ,
label.to_s.humanize.capitalize+
content_tag("span", warning),
:for => "#{#object_name}_#{label}")
+
super)
end
end
But no luck. Can anyone steer me in the right direction? Thanks, A
You need to call #template.content_tag. The code you have there is just calling self.content_tag, which obviously doesn't do anything.
Just wanted to post the final solution, more for pride than anything else. Noob... :0
def self.create_tagged_field(method_name)
define_method(method_name) do |label, *args|
# accepts the warning hash from text_field helper
if (args.first.instance_of? Hash) && (args.first.keys.include? :warning)
warning = args.first[:warning]
end
#template.content_tag("label" , label.to_s.humanize+(#template.content_tag("span", warning, :class =>'small')),
:for => "#{#object_name}_#{label}") +
super
end
end

Resources