Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP

Loading…

add html classes to inputs with the wrappers API #622

Closed
wants to merge 3 commits into from
@stephenprater

enable a user to add classes or other html attributes to inputs,
labels, and custome components using the wrappers API. this is
accomplished by passing false to the tag key of the wrap_with
options - like so

b.use :input, :wrap_with { :tag => false, :class => ['cool'] }

passing an argument with a false tag is only meaningful in the case
of a single wrapped component which generates a tag itself
(so, label and input is about it out of the box.) A custom
component which wants to support this functionality should look in
options[:#{namespace}_html] for the html options to use.

My particular use case for this is that I have about a dozen different
forms, but only three "looks" - and bootstrap has a few places where
you need to put classes on the input. Rather than have to use
a custom input type, just to add a class to the string input, i want
to create three different wrappers for my three different looks and
specify the wrapper once.

@rafaelfranca
Collaborator

I think you are distorting the wrappers API. :wrap_with is supposed to wrap an element, and in this case no wrapper are being created, and the classes are being added to the input.

I don't think that we should change the API to do this. I'm not happy with this implementation. Maybe allowing to pass classes to the component would be better. Something like:

b.use :input, :class => ['cool']
@rafaelfranca
Collaborator

@agrussellknives do you see any easy way to change your pull request to not change the wrappers API and pass the classes directly to the component?

I really don't want to change the wrappers API to this case. Your feature make sense but the wrappers API is complex and using it to change classes without add a wrapper will make more complex.

Also, thank you for the pull request.

@rafaelfranca
Collaborator

Some historial information. In #457 I change the wrappers API to not use :class and :tag directly to the component. The main reason was: the API was confusing and we don't know when it is creating a wrapper, or if it is using the classes directly.

I think that now we can change the API to pass the options directly to the component.

b.use :input, :class => ['cool']
# Add :class to the input component

b.use :input, :wrap_with => { :class => :cool }
# create a wrapper with the class 'cool' to wrap the input.

WDYT?

cc @carlosantoniodasilva @josevalim

@josevalim
Owner

@rafaelfranca it looks good, but we should be careful because a user my pass some options that are not recognized, for example:

b.use :placeholder, :class => ["cool"]

So we need a form of contract between the underlying input and this.

Finally, if we are improving this area, we should probably deprecate the others (less flexible) ways to customize the input classes.

@stephenprater

I've got no problem changing it for sure. I prefer @rafaelfranca 's API but didn't want to take it upon my self to un-depreciate things :)

I took my cue from the parameters to the wrapper method - where you can use false as the value for :tag to prevent it from generating a top level tag. The reason that you do THAT (correct me if I'm wrong) is so that you can turn off components (like placeholder) in a wrapped area without generating a surrounding tag correct? Should I change that too or is that an entirely separate issue? It's sort of confusing to me that it's okay in one instance but not in the other - but I don't think I'm fully understanding why it works that way in the the case of wrapper

@carlosantoniodasilva
Collaborator

@stephenprater you can give false to any component and it won't be included - ie label: false, or error: false.

@rafaelfranca I think it's a good approach to follow, would allow configuration per wrapper instead of global label/input classes for instance. Unsure about the need to deprecate the SimpleForm one's, since we could consider those as global to all wrappers?

@rafaelfranca
Collaborator

So you can pass :tag => false to wrappers (I don't know if it works with wrapper too) to disable the wrapper element. This make since, since wrappers (and wrapper) is creating a new tag around the block content.

But :wrap_with option is supposed to create a wrapper element around the component that you are passing the option. It is a shortcut to

b.wrapper :tag => 'div', :class => 'foo' do |x|
  x.use :input
end

So we can't use this option to change the component, or we will distort the wrappers API and make it more confusing.

@stephenprater

@rafaelfranca gotcha. makes sense now.

@rafaelfranca
Collaborator

@carlosantoniodasilva yes, we can leave the SimpleForm configuration.

@rafaelfranca
Collaborator

@stephenprater are you work on it? I think this is a good path to add more customization to SimpleForm

@stephenprater

@rafaelfranca yes, i'll work on it.

@stephenprater

@josevalim How and how strictly do you think this contract should be enforced? Ignore non-whitelisted attributes? Raise an exception? Print a warning? Any guidance is appreciated.

Current behavior just ignores things that don't make sense (like class on placeholder) and adds non standard HTML attributes for tag type components.

@rafaelfranca
Collaborator

@stephenprater seems good now. I think is better to use black list in this case. Print a warning if the component is
placeholder, error, hint, html5, maxlength, min_max, pattern and readonly. This will make possible to use this new API with custom components.

lib/simple_form/wrappers/single.rb
((9 lines not shown))
super(name, [name], options)
end
def render(input)
options = input.options
if options[namespace] != false
+ if [:input, :label_input].include? namespace
@rafaelfranca Collaborator

why :input is getting a different approach?

inputs keep their classes in a separate instance variable. it's duped from @html_classes in SimpleFrom::Inputs::Base#initialize - why it does that I couldn't tell you.

@rafaelfranca Collaborator

Ok. Just to confirm. I was not in my computer and didn't look at the code. Thanks to explain.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
lib/simple_form/wrappers/single.rb
((9 lines not shown))
super(name, [name], options)
end
def render(input)
options = input.options
if options[namespace] != false
+ if [:input, :label_input].include? namespace
+ input.input_html_classes.concat Array.wrap(wrapper_html_options[:class])
@carlosantoniodasilva Collaborator

no need to use Array.wrap (I mean, Array() only should be enough, right?)

@rafaelfranca Collaborator

I think we need. Kernel#Array doesn't play nice in Ruby 1.8

@carlosantoniodasilva Collaborator

Ow :shit:, I forgot we support 1.8 here :smile:.

Anyway, what should this :class option support? nil, [], and maybe 'foobar'? Those should be good I think?!?

>> Array(nil)
=> []
>> Array([])
=> []
>> Array('foo bar')
=> ["foo bar"]

Well, in any case @stephenprater, just ignore the Array.wrap comments, and let them out there, we probably have it in other places too :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
lib/simple_form/wrappers/single.rb
@@ -16,8 +27,18 @@ def render(input)
private
- def html_options(options)
- [:label, :input].include?(namespace) ? {} : super
+ def html_options_for_input(input, local_namespace = namespace)
+ (input.options["#{local_namespace}_html".intern] ||= {}).tap do |o|
+ if o[:class] or wrapper_html_options[:class]
+ o[:class] = Array.wrap(o[:class])
+ o[:class].concat Array.wrap(wrapper_html_options[:class])
@carlosantoniodasilva Collaborator

Ditto

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
lib/simple_form/wrappers/single.rb
@@ -16,8 +27,18 @@ def render(input)
private
- def html_options(options)
- [:label, :input].include?(namespace) ? {} : super
+ def html_options_for_input(input, local_namespace = namespace)
+ (input.options["#{local_namespace}_html".intern] ||= {}).tap do |o|
+ if o[:class] or wrapper_html_options[:class]
@carlosantoniodasilva Collaborator

|| instead of or

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
test/form_builder/wrapper_test.rb
@@ -150,6 +150,44 @@ class WrapperTest < ActionView::TestCase
end
end
+ test 'single element without tags applies options to component tags' do
+ swap_wrapper :default, self.custom_wrapper_with_no_wrapping_tag do
+ with_form_for @user, :name
+ assert_select "div.custom_wrapper div.elem input.input_class_yo"
+ assert_select "div.custom_wrapper div.elem input.other_class_yo"
+ assert_select "div.custom_wrapper div.elem input.string"
+ assert_select "div.custom_wrapper div.elem label[data-yo='yo']"
+ assert_select "div.custom_wrapper div.elem span.custom_yo", :text => "custom"
+ assert_select "div.custom_wrapper div.elem label.both_yo"
+ assert_select "div.custom_wrapper div.elem input.both_yo"
+ end
+ end
+
+ test 'single elment with wrap with and other options does both' do
+ swap_wrapper :default, self.custom_wrapper_with_no_wrapping_tag_and_wrapping_tag do
@carlosantoniodasilva Collaborator

there's no need for self, here in the other above.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
test/form_builder/wrapper_test.rb
((11 lines not shown))
+ assert_select "div.custom_wrapper div.elem span.custom_yo", :text => "custom"
+ assert_select "div.custom_wrapper div.elem label.both_yo"
+ assert_select "div.custom_wrapper div.elem input.both_yo"
+ end
+ end
+
+ test 'single elment with wrap with and other options does both' do
+ swap_wrapper :default, self.custom_wrapper_with_no_wrapping_tag_and_wrapping_tag do
+ with_form_for @user, :name
+ assert_no_select "div.custom_wrapper > input"
+ assert_select "div.custom_wrapper div.wrap input.input_class_yo"
+ assert_select "div.custom_wrapper div.wrap input.other_class_yo"
+ end
+ end
+
+
@carlosantoniodasilva Collaborator

two whitespaces :scissors:

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
test/form_builder/wrapper_test.rb
@@ -150,6 +150,44 @@ class WrapperTest < ActionView::TestCase
end
end
+ test 'single element without tags applies options to component tags' do
+ swap_wrapper :default, self.custom_wrapper_with_no_wrapping_tag do
+ with_form_for @user, :name
+ assert_select "div.custom_wrapper div.elem input.input_class_yo"
+ assert_select "div.custom_wrapper div.elem input.other_class_yo"
+ assert_select "div.custom_wrapper div.elem input.string"
+ assert_select "div.custom_wrapper div.elem label[data-yo='yo']"
+ assert_select "div.custom_wrapper div.elem span.custom_yo", :text => "custom"
+ assert_select "div.custom_wrapper div.elem label.both_yo"
+ assert_select "div.custom_wrapper div.elem input.both_yo"
+ end
+ end
+
+ test 'single elment with wrap with and other options does both' do
@carlosantoniodasilva Collaborator

typo element

@carlosantoniodasilva Collaborator

does both what? it's not clear to me what this test actually does :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
test/support/misc_helpers.rb
@@ -46,6 +46,32 @@ def custom_wrapper
end
end
+ def custom_wrapper_with_no_wrapping_tag
+ SimpleForm.build :tag => :div, :class => "custom_wrapper" do |b|
+ b.wrapper :tag => :div, :class => 'elem' do |component|
+ component.use :input, :class => ['input_class_yo', 'other_class_yo']
+ component.use :label, :"data-yo" => 'yo'
+ component.use :label_input, :class => 'both_yo'
+ component.use :custom_component, :class => 'custom_yo'
+ end
+ end
+ end
+
+ def custom_wrapper_with_no_wrapping_tag_and_wrapping_tag
@carlosantoniodasilva Collaborator

This name looks a little bit weird: with_no_wrapping_tag_and_wrapping_tag?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
lib/simple_form/wrappers/single.rb
@@ -16,8 +27,18 @@ def render(input)
private
- def html_options(options)
- [:label, :input].include?(namespace) ? {} : super
+ def html_options_for_input(input, local_namespace = namespace)
+ (input.options["#{local_namespace}_html".intern] ||= {}).tap do |o|
@carlosantoniodasilva Collaborator

There's no apparent need for tap here, since the result of this method is not used anywhere (the goal is to merge/concat stuff). I think it could get a bit clearer by using a simple variable instead of tap + block.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@stephenprater

Anything more I need to do to get this merged?

@rafaelfranca
Collaborator

@stephenprater I'll take a look and merge this tomorrow.

@rafaelfranca
Collaborator

@carlosantoniodasilva @josevalim @nashby do you guys agree with this path? I will make some refactorings after merge this one

lib/simple_form/wrappers/builder.rb
@@ -40,6 +40,11 @@ module Wrappers
# In the example above, hint defaults to false, which means it won't automatically
# do the lookup anymore. It will only be triggered when :hint is explicitly set.
class Builder
+
+ class_attribute :non_tag_components
+ self.non_tag_components = [:placeholder, :error, :hint, :html5,
@rafaelfranca Collaborator

I think this should be a constant since we are using only in this class

plugins, etc can add non tag producing components to wrappers, so if you wanted to be a good simpleform citizen, you'd add your component namespace this this list. I mean, I know you can modify the constant in place, since it's Ruby, but this would allow you to do something like this:

module SimpleForm
  module Components
    module Whatever
       SimpleForm::Wrapper::Builder.non_tag_components << :whatever

       def whatever
         input_html_options[:'data-whatever'] ||= whatever_value
         nil
       end

       def whatever_value
         options[:whatever]
       end
   end
end

That said, I could abstract this so you're not directly addressing the builder class. It seems that theoretically the responsibility to know whether or not it produces a tag should really lie with the component definition - so maybe we move all of this logic there? That sounds like a separate patch though.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
lib/simple_form/wrappers/builder.rb
@@ -52,14 +57,13 @@ def use(name, options=nil, &block)
return wrapper(name, options, &block)
end
- if options && options.keys != [:wrap_with]
- ActiveSupport::Deprecation.warn "Passing :tag, :class and others to use is deprecated. " \
- "Please invoke b.use #{name.inspect}, :wrap_with => #{options.inspect} instead."
- options = { :wrap_with => options }
+ if options && (self.class.non_tag_components.include?(name) \
+ && !(options.except(:wrap_with).keys.empty?))
+ warn "Invalid options #{options.except(:wrap_with).keys.inspect} passed to #{name}."
@rafaelfranca Collaborator

We not use the ActiveSupport::Deprecation.warn?

It's not a depreciation warning so much as a "you're using this wrong" warning - I wouldn't want this quashed by ActiveSupport::Depreciation behaviors that ignore depreciation warnings, on the other hand the more that I think about this - the more that I think it should probably just raise an exception. i can't think of any reason why you would ever want to do this.

@rafaelfranca Collaborator

Yes, raise an exception seems better.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
lib/simple_form/wrappers/builder.rb
@@ -52,14 +57,13 @@ def use(name, options=nil, &block)
return wrapper(name, options, &block)
end
- if options && options.keys != [:wrap_with]
- ActiveSupport::Deprecation.warn "Passing :tag, :class and others to use is deprecated. " \
- "Please invoke b.use #{name.inspect}, :wrap_with => #{options.inspect} instead."
- options = { :wrap_with => options }
+ if options && (self.class.non_tag_components.include?(name) \
+ && !(options.except(:wrap_with).keys.empty?))
@rafaelfranca Collaborator

We can still use the options.keys != [:wrap_with] check here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
lib/simple_form/wrappers/single.rb
@@ -2,13 +2,24 @@ module SimpleForm
module Wrappers
# `Single` is an optimization for a wrapper that has only one component.
class Single < Many
+ attr_reader :wrapper_html_options
+
def initialize(name, options={})
@rafaelfranca Collaborator
def initialize(name, options={})
  @wrapper_html_options = options.except(:wrap_with)
  super(name, [name], options.fetch(:wrap_with, {}))
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
lib/simple_form/wrappers/single.rb
@@ -16,6 +27,15 @@ def render(input)
private
+ def html_options_for_input(input, local_namespace = namespace)
+ options = ( input.options["#{local_namespace}_html".intern] ||= {} )
@rafaelfranca Collaborator

use to_sym here

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
lib/simple_form/wrappers/single.rb
@@ -16,6 +27,15 @@ def render(input)
private
+ def html_options_for_input(input, local_namespace = namespace)
@rafaelfranca Collaborator

This method is too confusing. What is the intent?

this merges all the different places that you could set classes with either more or less granularity into the particular input instance we are rendering, and sets up the options hash in the format expected by SimpleForm::Wrappers::Many#wrap

i'll add some comments to that affect.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@rafaelfranca
Collaborator

Sorry for the delay. I made some comments in the diff

@travisbot

This pull request passes (merged 57efd092 into 6d4af55).

@travisbot

This pull request passes (merged 8957072 into 46d4bb4).

lib/simple_form/wrappers/single.rb
((10 lines not shown))
end
def render(input)
options = input.options
if options[namespace] != false
+ #inputs store there html classes in a separate place from other tags
+ #or non-tag components - premerge them before collecting alll
+ #possible html_options
@carlosantoniodasilva Collaborator

Please fix these comments, add spaces after #, remove there, alll is wrong, finish the sentence with a .. 2 lines should be enough, thanks.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
lib/simple_form/wrappers/single.rb
@@ -16,6 +29,20 @@ def render(input)
private
+ def html_options_for_input(input, local_namespace = namespace)
+ # because there are a lot of different places where you can set classes
+ # on inputs (globally, on the individual input, in the form options, in
+ # the wrappers api, etc) we merge them all together here so that that
+ # SimpleForm::Wrappers::Many#wrap know what do with them, taking care
+ # to leave options[:class] as an array if necessary
+ options = ( input.options["#{local_namespace}_html".to_sym] ||= {} )
@carlosantoniodasilva Collaborator

Don't use spaces with ().

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
test/form_builder/wrapper_test.rb
((15 lines not shown))
+ end
+
+ test 'single element with wrap with and component options applies to both' do
+ swap_wrapper :default, custom_wrapper_with_wrapping_tag_and_component_options do
+ with_form_for @user, :name
+ assert_no_select "div.custom_wrapper > input"
+ assert_select "div.custom_wrapper div.wrap input.input_class_yo"
+ assert_select "div.custom_wrapper div.wrap input.other_class_yo"
+ end
+ end
+
+ test 'adding any option to tag components on the input ignores them' do
+ with_concat_form_for @user do |f|
+ concat f.input :name, :invalid => 'thing'
+ end
+ assert_no_select "input[invalid='thing']"
@carlosantoniodasilva Collaborator

You can just check for input[invalid], no need for thing

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@carlosantoniodasilva carlosantoniodasilva commented on the diff
test/form_builder/wrapper_test.rb
@@ -150,6 +150,52 @@ class WrapperTest < ActionView::TestCase
end
end
+ test 'single element without wrap_with applies options to component tags' do
+ swap_wrapper :default, custom_wrapper_with_no_wrapping_tag do
+ with_form_for @user, :name
+ assert_select "div.custom_wrapper div.elem input.input_class_yo"
+ assert_select "div.custom_wrapper div.elem input.other_class_yo"
+ assert_select "div.custom_wrapper div.elem input.string"
+ assert_select "div.custom_wrapper div.elem label[data-yo='yo']"
+ assert_select "div.custom_wrapper div.elem span.custom_yo", :text => "custom"
+ assert_select "div.custom_wrapper div.elem label.both_yo"
+ assert_select "div.custom_wrapper div.elem input.both_yo"
@carlosantoniodasilva Collaborator

assert_select accepts a block, may make this a bit more clear?

@carlosantoniodasilva Collaborator

Well, forget about it. Other tests need more clean up with that, we can do that later.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@carlosantoniodasilva
Collaborator

@stephenprater thank you for your work on that.

@rafaelfranca I'm ok with the feature, it is definitely useful. Feel free to do the changes you think that are required, I have made a few comments, but we'll need to review and give some more thought to the entire wrappers api afterwards.

Just want to note that I'd rather merge this after a new release.

Thanks guys! <3

@rafaelfranca rafaelfranca was assigned
@rafaelfranca
Collaborator

I'm going to merge this one this week.

@nashby your thoughts here would be great.

@nashby
Collaborator

@rafaelfranca Yes, I'm good with this feature too. Let's merge and we can refactor it a bit later. BTW, we can use non_tag_comopnents from this PR in #633 :)

@ChristianPeters

I am looking forward to it. :)

@zekus

guys, what happened with this pull request?

@rafaelfranca
Collaborator

Nothing, it is on my todo list to review, merge and refactor.

@lucaspiller

:+1: on having this merged in

@pusewicz

What is happening to this?

@rafaelfranca
Collaborator

nothing, I still need time to do it :wink:

@why-el

This needs to be merged.

@carlosantoniodasilva
Collaborator

@why-el thanks, we'll be handling this after we get 3.0 final out. I don't want to introduce big changes and new features right now, but it'll be part of Simple Form in the near future.

@mensfeld

still waiting guys ;)

@jcoleman

Any projected timeline on this?

@rafaelfranca
Collaborator

No. We will do when we can. We have a lot of things to do and after all this is open source, we are not being paid to work only on this project.

But be sure we want this in too. It is not the top priority to us. After the Rails 4 release and the Simple Form 3 we will revisit this like we said above.

Thanks.

@gmile

Guys, now that Bootstrap 3.0 RC1 is out, having this PR merged would be super useful. For the bootstrap form styling to propertly take place, it is now required that all inputs (including <textarea>, <select> etc.) have a .form-control class, and all labels to have .control-label class.

So far I see 2 options one could go with to workaroun current's SimpleForm limit:

  1. create custom replacements for all kinds of <input> tags (including <textarea>, <select> etc.) with the .form-control class set (as well as subclass default label to always include .control-label class),
  2. set desired .form-control ad-hoc for any input that requires it, as such (same for ):

    = f.input :title, input_html: { class: 'form-control' }

Yet a better option here would be exactly what this PR is suggesting – globally set classes on all inputs and labels inside a particular wrapper definition.

@stephenprater

rebased against current master - no more complicated merging and rebasing, this pull request she will now apply cleanly.

@sfaxon

+1 @stephenprater's rebased patch worked for me on a rails 4 app.

@TALlama

@gmile another option, if you're using SASS or LESS for styling, is to add a form-control-wrapper class to the input's wrapper with b.use :input, wrap_with: { class: 'form-control-wrapper' }, and then pull in the .form-control styles from the Bootstrap CSS files:

.form-control-wrapper {
  select, 
  textarea, 
  input[type="text"], 
  input[type="password"], 
  input[type="datetime"], 
  input[type="datetime-local"], 
  input[type="date"], 
  input[type="month"], 
  input[type="time"], 
  input[type="week"], 
  input[type="number"], 
  input[type="email"], 
  input[type="url"], 
  input[type="search"], 
  input[type="tel"], 
  input[type="color"] {
    .form-control /* for LESS */
    @extend .form-control /* for SASS */
  }
}

This is, to be very clear, hackish and brittle, and getting this PR merged would be better, but it allows you to use Bootstrap 3 now.

@stephenprater

@gmile - I'm using Bootstrap for this as well - so if you run into any problems let me know so I can update the PR.

@guigs

Thanks @stephenprater. I'm testing my app with your branch.
In case that the input needs an extra class, input_html: { class: 'extra' }, do you think the output should also contain default class (merged) or we must add both input_html: { class: 'extra form-control' }?

@carlosantoniodasilva carlosantoniodasilva referenced this pull request in rafaelfranca/simple_form-bootstrap
Closed

Support Bootstrap 3 #26

@jrhorn424

Hate to be a noob, but @stephenprater do you mean rebase against plataformatec's or your pull? And then download the patchfile and apply?

@stephenprater

I rebased against upstream (this fork) a couple of months ago and can get a clean apply of the patch, but if you want to use my fork just depend on it in your Gemfile (make sure you use the classes_on_use branch)

@joel joel referenced this pull request from a commit
Commit has since been removed from the repository and is no longer available.
@ZenCocoon

Is this pull request still of actuality? config.input_class have been merged, would be great to get this one as well to tackle the problem with more flexibility. (eg: with Bootstrap 3, have a form-control class on inputs but not checkboxes and radio buttons, which can be nicely done with custom wrappers.

@mainameiz

I think it would be nice if I could set attributes for specific elements
1) globally for all forms
2) globally for forms using a specific wrapper
3) inside a wrapper
(with merged attributes' values from previous level)

What is the reason of adding wrap_with option? It could be accomplished with

element.wrapper tag: 'div', class: 'el-class' do |wrapper| ... end

isn't it?

@umhan35

+1 for this

Stephen Prater and others added some commits
Stephen Prater add html classes to inputs with the wrappers API
enable a user to add classes or other html attributes to inputs,
labels, and custome components using the wrappers API.

   b.use :input, :class => ['cool'], :id => 'whatever' }

A custom component which wants to support this functionality
should look in `options[:#{namespace}_html]` for the html
options to use.

An ArgumentError is raised if an html attribue is passed to any
component in `SimpleForm::FormBuilder::ATTRIBUTE_COMPONENTS` but
otherwise no validation is done - so it's possible to pass invalid
attributes to any tag generating component.
2057126
@stephenprater stephenprater fix the use case of switching wrappers on a per input basis
  modified a test to cover this case and changed it to no longer
  memoize the input / label_html options hash. There's no big performance
  hit to doing so and it keeps the per input wrapper switching
  implementation clean
d72e99b
@stephenprater stephenprater Actually TEST the behavior thought to to be tested in the #c02f95
  also, fix the behavior by making sure that wrapper options passed to a
  particular input override the options passed to the original builder in
  `SimpleForm::Inputs::Base#html_options_for` - this might be an existing
  bug exposed by this test since changing the code in this manner causes
  no test failures - and at least the behavior is now codified in a test
5f29ada
@stephenprater

@rafaelfranca i hate to be an ass about this - but is there ANY CHANCE this could get merged? is there something i can do to help you?

@mchlfhr

+1

Would lead to soo much cleaner code. I love to use different wrappers for different forms (especially for forms which have other dimensions in the Bootstrap 3 grid). But the only way to do so is currently (please correct me if I am wrong):

b.use :label , wrap_with: { tag: 'div', class: 'col-md-2 text-right' }
b.use :input , wrap_with: { tag: 'div', class: 'col-md-10' }

Shorter and no additional "div" around my label

b.use :label, :class => ['col-md-2']
b.use :input, :class => ['col-md-10']
@rafaelfranca
Collaborator

Yes, this will be merged. As you can see it is in the 3.1 milestone.

@rafaelfranca rafaelfranca was assigned
@rafaelfranca
Collaborator

@stephenprater hey, I merged this PR in a separate branch and I found some problems.

The error classes and hint classes are getting duplicated. I think you had the same problem with input classes (this is why I think you called uniq there. Could you take a look?

@jrust

:+1: on this. I am currently using the rm-attributes-to-components branch to get bootstrap 3 horizontal forms working nicely. With this wrapper they work and are decently flexible:

  config.wrappers :horizontal, tag: :div, class: 'form-group', error_class: 'has-error' do |b|
    b.use :html5
    b.use :placeholder
    b.use :min_max
    b.use :maxlength

    b.optional :pattern
    b.optional :readonly

    b.use :label, class: 'col-md-2'
    b.wrapper :right_col, tag: :div, class: 'col-md-5' do |component|
      component.use :input
      component.use :hint,  wrap_with: { tag: 'p', class: 'help-block' }
      component.use :error, wrap_with: { tag: 'span', class: 'help-block has-error' }
    end
  end

The form column widths can then be overridden via the defaults in the simple_form tag:

simple_form_for @user, wrapper: :horizontal, html: { class: 'form-horizontal' }, defaults: { label_html: { class: 'col-md-3' }, right_col_html: { class: 'col-md-6' } } do |f|

Only caveat is that while the label_html col-md-4 default overrides the col-md-2 from the wrapper, the right_col_html default appends col-md-6 to the wrapper's col-md-5. Which means I can make right column bigger, but not smaller since md-6 overrides md-5. It would be nice if there were some way override rather than augment the right_col class, but for now at least its working!

@kjs3

+1

@butsjoh

We made some changes (younited/simple_form@ffecbe0) to have a consistent behaviour for the merging of html attributes. We made the following changes (happened on the 2.2 branch since we cannot move to 3 yet):

  • We replaced the normal merge behaviour with deep_merge where applicable (eg: if f.input has defined a tabindex and and the wrapper adds a default class both are applied)
  • We assumed that specifying the options on the input directly (using f.input) take precedence over the defaults configured on the form (defaults setting on form builder) and as a last resort it will use the ones defined on the wrappers.
  • We assume that all the config options of the wrapper are used when defining a wrapper directly on an input
@Yankie

+1

@jsmestad

:+1: this is extremely helpful when integrating simple_form with Foundation forms.

@swils swils referenced this pull request from a commit
Commit has since been removed from the repository and is no longer available.
@rafaelfranca rafaelfranca closed this in #997
@mru2 mru2 referenced this pull request from a commit in jobteaser/simple_form
@mru2 mru2 Backport pull request #622 to simple_form 2.x 7d18fb1
@jobteaser jobteaser referenced this pull request in jobteaser/simple_form
Merged

Backport pull request #622 to simple_form 2.1.x #1

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on Nov 9, 2013
  1. @stephenprater

    add html classes to inputs with the wrappers API

    Stephen Prater authored stephenprater committed
    enable a user to add classes or other html attributes to inputs,
    labels, and custome components using the wrappers API.
    
       b.use :input, :class => ['cool'], :id => 'whatever' }
    
    A custom component which wants to support this functionality
    should look in `options[:#{namespace}_html]` for the html
    options to use.
    
    An ArgumentError is raised if an html attribue is passed to any
    component in `SimpleForm::FormBuilder::ATTRIBUTE_COMPONENTS` but
    otherwise no validation is done - so it's possible to pass invalid
    attributes to any tag generating component.
  2. @stephenprater

    fix the use case of switching wrappers on a per input basis

    stephenprater authored
      modified a test to cover this case and changed it to no longer
      memoize the input / label_html options hash. There's no big performance
      hit to doing so and it keeps the per input wrapper switching
      implementation clean
  3. @stephenprater

    Actually TEST the behavior thought to to be tested in the #c02f95

    stephenprater authored
      also, fix the behavior by making sure that wrapper options passed to a
      particular input override the options passed to the original builder in
      `SimpleForm::Inputs::Base#html_options_for` - this might be an existing
      bug exposed by this test since changing the code in this manner causes
      no test failures - and at least the behavior is now codified in a test
This page is out of date. Refresh to see the latest.
View
5 lib/simple_form/components/label_input.rb
@@ -8,6 +8,11 @@ module LabelInput
end
def label_input
+ [:input_html, :label_html].each do |key|
+ if options.has_key? key
+ options[key].merge! options.fetch(:label_input_html, {})
+ end
+ end
options[:label] == false ? input : (label + input)
end
end
View
2  lib/simple_form/components/labels.rb
@@ -46,7 +46,7 @@ def label_html_options
if options.key?(:input_html) && options[:input_html].key?(:id)
label_options[:for] = options[:input_html][:id]
end
- label_options
+ @label_options = label_options
end
protected
View
10 lib/simple_form/inputs/base.rb
@@ -65,15 +65,20 @@ def initialize(builder, attribute_name, column, input_type, options = {})
@html_classes = SimpleForm.additional_classes_for(:input) { additional_classes }
@input_html_classes = @html_classes.dup
+ @input_html_options = {}
+
if SimpleForm.input_class && !input_html_classes.empty?
input_html_classes << SimpleForm.input_class
end
+ end
- @input_html_options = html_options_for(:input, input_html_classes).tap do |o|
+ def input_html_options
+ html_options = html_options_for(:input, input_html_classes).tap do |o|
o[:readonly] = true if has_readonly?
o[:disabled] = true if has_disabled?
o[:autofocus] = true if has_autofocus?
end
+ @input_html_options.merge! html_options
end
def input
@@ -126,8 +131,9 @@ def reflection_or_attribute_name
def html_options_for(namespace, css_classes)
html_options = options[:"#{namespace}_html"]
html_options = html_options ? html_options.dup : {}
+ html_options.reverse_merge!(@builder.wrapper.options[:"#{namespace}_html"] || {})
css_classes << html_options[:class] if html_options.key?(:class)
- html_options[:class] = css_classes unless css_classes.empty?
+ html_options[:class] = css_classes.uniq unless css_classes.empty?
html_options
end
View
14 lib/simple_form/wrappers/builder.rb
@@ -40,14 +40,24 @@ module Wrappers
# In the example above, hint defaults to false, which means it won't automatically
# do the lookup anymore. It will only be triggered when :hint is explicitly set.
class Builder
+
def initialize(options)
@options = options
@components = []
end
def use(name, options=nil, &block)
- if options && wrapper = options[:wrap_with]
- @components << Single.new(name, wrapper)
+ if options && (SimpleForm::FormBuilder::ATTRIBUTE_COMPONENTS.include?(name) \
+ && !(options.except(:wrap_with).keys.empty?))
+ raise ArgumentError, "Invalid options #{options.except(:wrap_with).keys.inspect} passed to #{name}."
+ end
+
+ if options && options[:wrap_with]
+ @options[:"#{name}_html"] = options.except(:wrap_with)
+ @components << Single.new(name, options[:wrap_with])
+ elsif options
+ @options[:"#{name}_html"] = options
+ @components << name
else
@components << name
end
View
58 test/form_builder/wrapper_test.rb
@@ -164,6 +164,18 @@ class WrapperTest < ActionView::TestCase
assert_select "section.custom_wrapper div.another_wrapper input.string"
end
+ test 'wrappers with use classes on input basis with arrays' do
+ swap_wrapper :default do
+ with_form_for @user, :name
+ assert_select "section.custom_wrapper div.class1.class2 input.class3.class4"
+
+ output_buffer.replace ""
+
+ with_form_for @user, :name, wrapper: custom_wrapper_with_no_wrapping_tag
+ assert_select "div.custom_wrapper div.elem input.input_class_yo.other_class_yo"
+ end
+ end
+
test 'access wrappers with indifferent access' do
swap_wrapper :another do
with_form_for @user, :name, wrapper: "another"
@@ -172,6 +184,52 @@ class WrapperTest < ActionView::TestCase
end
end
+ test 'single element without wrap_with applies options to component tags' do
+ swap_wrapper :default, custom_wrapper_with_no_wrapping_tag do
+ with_form_for @user, :name
+ assert_select "div.custom_wrapper div.elem input.input_class_yo"
+ assert_select "div.custom_wrapper div.elem input.other_class_yo"
+ assert_select "div.custom_wrapper div.elem input.string"
+ assert_select "div.custom_wrapper div.elem label[data-yo='yo']"
+ assert_select "div.custom_wrapper div.elem span.custom_yo", :text => "custom"
+ assert_select "div.custom_wrapper div.elem label.both_yo"
+ assert_select "div.custom_wrapper div.elem input.both_yo"
@carlosantoniodasilva Collaborator

assert_select accepts a block, may make this a bit more clear?

@carlosantoniodasilva Collaborator

Well, forget about it. Other tests need more clean up with that, we can do that later.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
+ end
+ end
+
+ test 'single element with wrap with and component options applies to both' do
+ swap_wrapper :default, custom_wrapper_with_wrapping_tag_and_component_options do
+ with_form_for @user, :name
+ assert_no_select "div.custom_wrapper > input"
+ assert_select "div.custom_wrapper div.wrap input.input_class_yo"
+ assert_select "div.custom_wrapper div.wrap input.other_class_yo"
+ end
+ end
+
+ test 'adding any option to tag components on the input ignores them' do
+ with_concat_form_for @user do |f|
+ concat f.input :name, :invalid => 'thing'
+ end
+ assert_no_select "input[invalid]"
+ end
+
+ test 'adding any option to tag components in wrapper makes html attributes' do
+ swap_wrapper :default, custom_wrapper_with_wrapping_tag_and_invalid_attributes do
+ with_input_for @user, :name, :string, :other_invalid => 'other_thing'
+ assert_select "input.input_class_yo"
+ assert_select "input[invalid='thing']"
+ assert_no_select "input[other_invalid]"
+ end
+ end
+
+ test 'adding invalid options to non-tag components raises an exception' do
+ assert_raise ArgumentError, "Invalid options [:class] passed to placeholder." do
+ swap_wrapper :default, custom_wrapper_with_invalid_options do
+ with_form_for @user, :name
+ end
+ end
+ end
+
test 'do not duplicate label classes for different inputs' do
swap_wrapper :default, self.custom_wrapper_with_label_html_option do
with_concat_form_for(@user) do |f|
View
48 test/support/misc_helpers.rb
@@ -61,6 +61,10 @@ def custom_wrapper
ba.use :label
ba.use :input
end
+ b.wrapper :arrays, class: ["class1", "class2"] do |ba|
+ ba.use :input, class: ["class3", "class4"]
+ ba.use :label
+ end
b.wrapper :error_wrapper, tag: :div, class: "error_wrapper" do |be|
be.use :error, wrap_with: { tag: :span, class: "omg_error" }
end
@@ -68,6 +72,37 @@ def custom_wrapper
end
end
+ def custom_wrapper_with_no_wrapping_tag
+ SimpleForm.build :tag => :div, :class => "custom_wrapper" do |b|
+ b.wrapper :tag => :div, :class => 'elem' do |component|
+ component.use :input, :class => ['input_class_yo', 'other_class_yo']
+ component.use :label, :"data-yo" => 'yo'
+ component.use :label_input, :class => 'both_yo'
+ component.use :custom_component, :class => 'custom_yo'
+ end
+ end
+ end
+
+ def custom_wrapper_with_wrapping_tag_and_component_options
+ SimpleForm.build :tag => :div, :class => 'custom_wrapper' do |b|
+ b.use :input, :class => ['input_class_yo', 'other_class_yo'],
+ :wrap_with => { :tag => :div, :class => 'wrap' }
+ end
+ end
+
+ def custom_wrapper_with_wrapping_tag_and_invalid_attributes
+ SimpleForm.build :tag => :div, :class => "custom_wrapper" do |b|
+ b.use :input, :class => 'input_class_yo', :invalid => 'thing'
+ end
+ end
+
+ def custom_wrapper_with_invalid_options
+ SimpleForm.build :tag => :div, :class => "custom_wrapper" do |b|
+ b.use :placeholder, :class => 'no_effect'
+ b.use :input
+ end
+ end
+
def custom_wrapper_with_wrapped_input
SimpleForm.build tag: :div, class: "custom_wrapper" do |b|
b.wrapper tag: :div, class: 'elem' do |component|
@@ -158,3 +193,16 @@ def input(attribute_name, *args, &block)
class CustomMapTypeFormBuilder < SimpleForm::FormBuilder
map_type :custom_type, to: SimpleForm::Inputs::StringInput
end
+
+module SimpleForm::Components::CustomComponent
+ def custom_component
+ @custom_component ||= begin
+ custom_options = options[:custom_component_html]
+ template.content_tag(:span, custom_options) do
+ "custom".html_safe
+ end
+ end
+ end
+end
+
+SimpleForm::Inputs::Base.send(:include, SimpleForm::Components::CustomComponent)
Something went wrong with that request. Please try again.