Rails passing a block to a helper method - ruby-on-rails

Viget Labs posted an article and gist detailing a rails helper method for adding a particular class (like .selected or .active) to a navigation link if it's url matches the current path.
You can use it in your layout like so:
= nav_link "News", articles_path, {class: "btn btn-small"}
<!-- which creates the following html -->
News
Nice. I'm using bootstrap, and want to have an icon in my button, so I need to generate the following html:
<i class="icon-home"> </i> News
I forked the gist and figured out a simple way of doing it. My fork lets the developer pass :inner_html and :inner_class to the helper like so:
= nav_link "News", articles_path, {class: "btn btn-small"}, {inner_html: 'i', inner_class: 'icon-home'}
It works fine, but I don't like my underlying implementation:
def link
if #options[:inner_html]
link_to(#path, html_options) do
content_tag(#options[:inner_html], '', :class => #options[:inner_class]) + " #{#title}"
end
else
link_to(#title, #path, html_options)
end
end
As you can see, I'm passing the new options to content_tag inside the block of a link_to method. I was hoping I would be able to refactor it in a few ways.
First of all, I'd prefer to be able to do this in my view:
= nav_link "News", articles_path, {class: "btn btn-small"} do
%i.icon-home
I want to give the inner html as a block, and not as attributes of the option hash. Can anyone give me any pointers on how to achieve this?
I thought it would a simple case of telling the nav_link method to accept a block:
def nav_link(title, path, html_options = {}, options = {}, &block)
LinkGenerator.new(request, title, path, html_options, options, &block).to_html
end
class LinkGenerator
include ActionView::Helpers::UrlHelper
include ActionView::Context
def initialize(request, title, path, html_options = {}, options = {}, &block)
#request = request
#title = title
#path = path
#html_options = html_options
#options = options
#block = block
end
def link
if #block.present?
link_to #path, html_options do
#block.call
#title
end
end
end
But this fails to output the icon, and instead inserts a number (4). I don't get it clearly. Anyone got any advice. Where can I go to read more about this sort of thing, as I really want to be able to figure stuff like this out without having to ask on stackoverflow.

I tried your problem and the following worked for me perfectly, in the helper:
def my_link(title, path, &block)
if block_given?
link_to path do
block.call
concat(title)
end
else
link_to title, path
end
end
Usage:
my_link "No title", User.first do
%i.icon-home

The solution in the end was as follows:
# capture the output of the block, early on if block_given?
def nav_link(title, path, html_options = {}, options = {}, &block)
LinkGenerator.new(request, title, path, html_options, options, (capture(&block) if block_given?)).to_html
end
I also had to modify my link method:
def link
if #block.present?
link_to(#path, html_options) do
#block.concat(#title)
end
else
link_to(#title, #path, html_options)
end
end
I've updated my gist. You could probably hack it up to accept more complex blocks.

Related

sending multiple parameters to another function in ruby

I'm trying to create a function that adds some functionality to the link_to function in rails. What I'd like it to do is simply to add a class to it. What I have so far:
#application_helper.rb
def button_link(*args)
link_to(*args.push(class: 'btn'))
end
Problem is that if I now add another class to the button_link function it doesn't work.
Example:
<td class='button'>
<%= button_link "Show", category_path(item), class: "btn-primary" %>
</td>
I get the following error: wrong number of arguments (4 for 3). How can I do this correctly?
link_to has 4 method signatures.. This is the one used most often.
Below we check to see if a class was already sent in -- and because of how HTML classes work, we want to have multiple classes, which are space-separated values.
def button_link(body, url, html_options={})
html_options[:class] ||= ""
html_options[:class] << " btn"
link_to body, url, html_options
end
The other method signatures can be viewed http://api.rubyonrails.org/classes/ActionView/Helpers/UrlHelper.html#method-i-link_to
Try changing your helper method to this, trying to maintain the link_to form:
def button_link(name, url_options, html_options = {})
if html_options.has_key?(:class)
css_options = html_options.fetch(:class)
css_options << ' current'
html_options.merge!( { :class => css_options } )
else
html_options.merge!( { :class => ' btn' } )
end
link_to(name, url_options, html_options)
end

Helper wrapping link_to with an html attribute

I'd like to have a helper that works just like link_to except that it merges in a data attribute (in this case for ease of creating tabs using bootstrap: http://twitter.github.com/bootstrap/javascript.html#tabs)
So I can call it like this:
link_to_tab("Name", #obj)
and get
<a href='/path' data-toggle='tab'>Name</a>
I've come up with this which seems to work:
def link_to_tab(*args, &block)
toggle_hash = {'data-toggle' => 'tab'}
last_arg = args.pop # if link_to was given a hash of html_options, merge with it
if last_arg.is_a? Hash
link_to(*args, last_arg.merge(toggle_hash), &block)
else
link_to(*args, last_arg, toggle_hash, &block)
end
end
Is there a cleaner, more idiomatic way to support all of the styles of calling link_to?
Not really. You could try this...
def link_to_tab(*args, &block)
toggle_hash = {'data-toggle' => 'tab'}
if args.last.is_a? Hash
args.last.merge!(toggle_hash)
else
args << toggle_hash
end
link_to(*args, &block)
end
Not that different though...
I'd like to have a helper that works just like link_to except that it merges in a data attribute
I might be missing something, but why not just pass a custom data argument to the link_to helper?
= link_to "foo tab", {}, "data-toggle" => "tab"
Outputs:
<a data-toggle="tab" href="/">foo tab</a>
Edit
If you're planning on using it a lot you can do:
def link_to_tab(*args, &block)
if args.last.is_a? Hash
link_to *(args.take args.size - 1), args.last.merge("data-tab" => "tab"), &block
else
link_to *args, "data-tab" => "tab", &block
end
end

Custom form builder not invoked in the fields_for method

I have a nested form with 3 related models. I want all the fields to be built by my custom form builder PanelFormBuilder. However only the form_for seems to be processed by my form builder and the fields_for doesn't - it gets processed by the default one. I can't for the life of me work out why.
This works:
<%= panel_form_for #firm do |f| %>
This doesn't:
<%= panel_fields_for #firm.company do |c| %>
Here are the custom methods:
module ApplicationHelper
def panel_form_for(object, options = {}, &block)
options[:builder] = PanelFormBuilder
form_for(object, options, &block)
end
def panel_fields_for(object, options = {}, &block)
options[:builder] = PanelFormBuilder
fields_for(object, options, &block)
end
end
Here is the custom form builder
class PanelFormBuilder < ActionView::Helpers::FormBuilder
delegate :content_tag, :tag, to: :#template
%w[text_field text_area password_field].each do |method_name|
define_method(method_name) do |name, *args|
options = args.extract_options!
options.merge!(:class => "required")
super(name, *(args + [options]))
end
end
end
Can anyone tell me what I am doing wrong?
Thanks
Found my own answer. Turns out I should have been doing this:
<%= panel_form_for #firm do |f| %>
Followed by:
<%= f.fields_for :company do |c| %>
I don't need the panel_fields_for at all.
Look at the definition of fields_for:
fields_for(record_name, record_object = nil, options = {}, &block)
When you call fields_for with the block as an argument, Ruby will not be able to figure out that the options parameter you specify is actually the options, but instead think it is the record_object parameter.
You can just pass nil as the second parameter, and your custom builder will be instantiated:
fields_for(object, nil, options, &block)
This can be good to know in situations where you don't have access to the builder object of the form, like when you create some of the fields from a separate yield block.

Rail3 - Smart way to Display Menu & add an Active Style Class?

Given a Rails 3 App with a menu like:
<ul>
<li>Home</li>
<li>Books</li>
<li>Pages</li>
</ul>
What is a smart way in Rails to have the app know the breadcrumb,,, or when to make one of the LIs show as:
<li class="active">Books</li>
thx
I'm not sure if I will provide you with a smart way but better something than nothing...
If your menu has some links - it is not in your example but I suppose that real menu should have links, not just the items. For example something like this in HAML: (I'm using HAML as writing ERB in text area is pure hell)
%ul
%li= link_to "Home", :controller => "home"
%li= link_to "Books", :controller => "books"
%li= link_to "Pages", :controller => "pages"
Then this helper (pasted from my project) should come handy:
#
# Shows link with style "current" in case when the target controller is same as
# current
# beware: this helper has some limitation - it only accepts hash as URL parameter
#
def menu_link_to(title, url, html_options = {})
unless url.is_a?(Hash) and url[:controller]
raise "URL parameter has to be Hash and :controller has to be specified"
end
if url[:controller] == controller.controller_path
html_options[:class] = "current"
end
link_to(title, url, html_options)
end
With this helper you can replace your "link_to" in the code above with "menu_link_to" and that's it!
An modified version of Radek Paviensky's helper is a tad simpler and more similar to link_to.
# Shows link with style "current" in case when the target controller is same as
# current.
def menu_link_to(title, options = {}, html_options = {})
if current_page?(options)
html_options[:class] ||= []
html_options[:class] << "active" # #TODO catch cases where the class is passed as string instead of array.
end
link_to(title, options, html_options)
end

How to automatically set all links to nofollow in Rails

I know I can pass :rel => "nofollow" to link_to but is there a way to set that by default so I don't have to make changes in each link_to tag?
In your application helper you can override the link_to method and replace with your own.
def link_to(name, options = {}, html_options = {})
html_options.merge!(:rel => :nofollow)
super(name, options, html_options)
end
You could create an alias to the old link_to then override it so it calls the old alias with the extra parameter. That way, you don't have to change all the existing link_to in your code.

Resources