How to extend a core Rails FormBuilder field - ruby-on-rails

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

Related

Adding a class to a helper method call

So I have a helper method that I am trying to apply css to without putting it in a div or any other element. How would I go about applying the css class to this helper in rails?
I tried:
<%= first_letter_content(e.content), :class => "first-letter" %>
and
<%= (first_letter_content(e.content), :class => "first-letter") %>
both resulting in syntax error, unexpected ',', expecting ')'
Helper code:
def first_letter_content(content)
first_letter = content[0]
return first_letter
end
Any suggestions? I have been trying to find the proper syntax, but no luck.
Your helper does not support options (extra args) but you are trying to give a HTML class to the element.
You should wrap the content of first_letter_content inside a div/span (depending on what you want, block or inline) and apply the class on this HTML element:
<div class='first-letter'>
<%= first_letter_content(e.content) %>
</div>
Or you can directly wrap the content[0] inside a div in the helper method:
def first_letter_content(content, options = {})
content_tag(:div, content[0], options)
end
And use it like this:
first_letter_content(content, class: 'first-letter')
first_letter_content(content, class: 'first-letter', id: 'something')
first_letter_content(content)
Also, you can refactor your helper method to this:
def first_letter_content(content)
content[0]
end
It is a minor improvement but in Ruby the "last thing" used in a method will be returned by this method.
Examples:
def something
a = 2
b = 3
a
end
# => returns `2`
def something_else
a = 2
b = 3
end
# => returns `3`
def whatever
a = 12
nil
end
# => returns `nil`
I am trying to apply css to without putting it in a div or any other element
Css classes are for DOM elements, so you should wrap this content into some element/node.
For example:
def first_letter_content(content, css_class)
content_tag(:div, content[0], class: css_class)
end
Call:
<%= first_letter_content(e.content, "first-letter") %>

Cannot capture output of block in Rails helper

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

How to get class="form-control" in input fields by default (Rails Form Helper + Bootstrap 3)

I'm converting my site to Twitter Bootstrap 3, and have run into what seems like silly problem, but I haven't been able to find an easy solution via google.
How do I get class="form-control" to be populated by default in the Rails Form Helper? I can only do it by typing it explicitly, this seems like a waste of time. (below)
It is required for bootstrap to style the input.
<%= f.label :email %>
<%= f.text_field :email, class: "form-control" %>
Am I naive to think that Rails should add this feature just because bootstrap implemented it?
Yup, this can be done without changing the way you use the Rails form helpers. You can extend the form helpers to include the class name if it is not already included in the options.
Note: You will have to override each method in FormTagHelper that you want to augment. This only augments text_field_tag.
Add something like this to your ApplicationHelper:
module ApplicationHelper
module BootstrapExtension
FORM_CONTROL_CLASS = "form-control"
# Override the 'text_field_tag' method defined in FormTagHelper[1]
#
# [1] https://github.com/rails/rails/blob/master/actionview/lib/action_view/helpers/form_tag_helper.rb
def text_field_tag(name, value = nil, options = {})
class_name = options[:class]
if class_name.nil?
# Add 'form-control' as the only class if no class was provided
options[:class] = FORM_CONTROL_CLASS
else
# Add ' form-control' to the class if it doesn't already exist
options[:class] << " #{FORM_CONTROL_CLASS}" if
" #{class_name} ".index(" #{FORM_CONTROL_CLASS} ").nil?
end
# Call the original 'text_field_tag' method to do the real work
super
end
end
# Add the modified method to ApplicationHelper
include BootstrapExtension
end
To get the class added to all form elements, even if those form elements are generated by gems like simple_form, the modification has to be done on a higher-level class than the ApplicationController. The following snippet can be placed in an initializer to do just that:
require 'action_view/helpers/tags/base'
# Most input types need the form-control class on them. This is the easiest way to get that into every form input
module BootstrapTag
FORM_CONTROL_CLASS = 'form-control'
def tag(name, options, *)
options = add_bootstrap_class_to_options options, true if name.to_s == 'input'
super
end
private
def content_tag_string(name, content, options, *)
options = add_bootstrap_class_to_options options if name.to_s.in? %w(select textarea)
super
end
def add_bootstrap_class_to_options(options, check_type = false)
options = {} if options.nil?
options.stringify_keys!
if !check_type || options['type'].to_s.in?(%w(text password number email))
options['class'] = [] unless options.has_key? 'class'
options['class'] << FORM_CONTROL_CLASS if options['class'].is_a?(Array) && !options['class'].include?(FORM_CONTROL_CLASS)
options['class'] << " #{FORM_CONTROL_CLASS}" if options['class'].is_a?(String) && options['class'] !~ /\b#{FORM_CONTROL_CLASS}\b/
end
options
end
end
ActionView::Helpers::Tags::Base.send :include, BootstrapTag
ActionView::Base.send :include, BootstrapTag
Yes, it's a waste of time.
Use simple_form gem which integrate nicely with Bootstrap. You no longer need to write these.
After bundle, just run
rails generate simple_form:install --bootstrap
Then a simple_form initailizer will be added. You can further customize it in initializers/simple_form_bootstrap, though default is good enough.
All these helper classed will be generated automatically, as well as many other good stuff.
You can use one of the Bootstrap related Gems such as this one:
https://github.com/stouset/twitter_bootstrap_form_for
or this one:
https://github.com/sethvargo/bootstrap_forms
The previous two answers will indeed work very well (simple form using your own initializer or use the bootstrap form gems). As with anything in code, there are many ways to skin a cat. Another (more manual) way is to add a form helper of your own. The steps are basically:
Create a helper file such as app/helpers/custom_form_helper.rb
Inherit from the form builder class: CustomFormBuilder < ActionView::Helpers::FormBuilder
Create the look you want.
def text_field(label, *args)
options = args.extract_options!
new_class = options[:class] || "form-control"
super("dd", label, *(args << options.merge(:class => new_class)))
end
Call your helper method in application so you don not have to include the helper each time you call a form, like:
def custom_form_for(name, *args, &block)
options = args.extract_options!
content_tag("div",
content_tag("dl", form_for(name, *(args << options.merge(:builder => CustomFormBuilder)), &block)), :class => "standard_form")
end
Use the custom form in your forms as custom_form_for

Rails - Add a method to a textfield

I'm trying to get the checkSwear method to run on each textfield before it's submitted..
I have basically this: (stripped down)
<%= form_for(#profile) do |f| %>
<div class="field">
<%= f.label 'I love to ' %>
<%= f.text_field :loveTo %>
</div>
<div class="field">
<%= f.label 'I hate to ' %>
<%= f.text_field :hateTo %>
</div>
<div class="actions">
<%= f.submit %>
</div>
<% end %>
In my controller I have:
def checkSwear
antiSwear.checkSwear(What goes here?)
end
In routes:
match '/check' => 'profiles#checkSwear'
Any help much appreciated!
(checkSwear is a separate gem; i.e. a separate problem! The what does here means what kind of variable is received from the form, to be put through the checkswear gem)
UPDATE:
Sorry for the camelcasing, I'm a Java developer studying Rails etc., old habits die hard. This is for a project. I'm supposed to be writing a small gem to do some ruby logic and apply it to something. The contents of the gem are:
module antiSwear
#swearwords = ["f**k", "f***ing", "shit", "shitting", "lecturer"]
#replacements = ["fornicate", "copulating", "poop", "pooping", "Jonathan"]
def self.checkText(text)
#swearwords.each do |swearword|
if text.include?(swearword)
index = #swearwords.index(swearword)
replacement = #replacements[index]
text.gsub(swearword, replacement)
end
end
return text
end
end
:/
This should really be done in model validations.
class Profile < ActiveRecord::Base
validate :deny_swearing
private
def deny_swearing
if AntiSwear.check_swear(love_to) || AntiSwear.check_swear(hate_to)
errors.add_to_base('Swearing is not allowed.')
end
end
end
That said, if you insist on this being in controller, you can check params[:profile][:love_to] and params[:profile][:hate_to] to see what's been submitted.
P.S. In this example I used proper ruby naming conventions, since we don't use "camelCasing".
Are you doing this as part of validation? You can do it one of a few ways. You can run the check before save, via a custom validation method or override the setter directly. I show you the custom validation approach here:
class Profile < ActiveRecord::Base
validate :clean_loveTo
protected
def clean_loveTo
errors.add(:loveTo, "can't contain swears") if antiSwear.checkSwear(loveTo)
end
end
I'm assuming checkSwear returns a boolean here.
I'd use an intersection on arrays, one of which is the source text split into words, then gsub the replacements in. You have to be sure to have a 1:1 relationship between the words and their replacements, in which case I'd suggest using a hash for your dictionary (coincidentally what hashes are sometimes called in other languages).
module antiSwear
# var names changed for formatting
#swears = ["f**k", "f***ing", "shit", "shitting", "lecturer"]
#cleans = ["fornicate", "copulating", "poop", "pooping", "Jonathan"]
def self.checkText(text)
# array intersection. "which elements do they have in common?"
bad = #swears & text.split # text.split = Array
# replace swear[n] with clean[n]
bad.each { |badword| text.gsub(/#{badword}/,#cleans[#swears.index(badword)] }
end
end
You might need to futz with text.split arguments if the replacement gets hung up on \n & \r stuff.

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