Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add TailwinCSS 3 support #1762

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
49 changes: 46 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,50 @@ You will need to provide your own CSS styles for hints.

Please see the [instructions on how to install Foundation in a Rails app](http://foundation.zurb.com/docs/applications.html).

### TailwindCSS 3

To generate wrappers that are compatible with [TailwindCSS 3](https://tailwindcss.com/), pass
the `tailwindcss` option to the generator, like this:

```console
rails generate simple_form:install --tailwindcss
```

Then add to your `tailwind.config.js` following config:

```
module.exports = {
content: [
'./config/initializers/simple_form_tailwindcss.rb'
]
}
```

You have to be sure that you have a [tailwindcss-rails](https://github.com/rails/tailwindcss-rails) gem.

**Warnings**

Please pay attention that Tailwindcss not have owned a file-input style and with it, we can't modify `abbr` (star for required fields), so for styling, just add the next styles to your `app/assets/application.tailwind.css` or if you have a gem [tailwindcss_merger](https://github.com/loqimean/tailwindcss_merger) (for multiple files of tailwind's' additional styles) to `app/assets/tailwindcss_stylesheets/your_styles.css`:

*For file-input:*

```
input[type=file]::-webkit-file-upload-button,
input[type=file]::file-selector-button {
@apply text-white bg-indigo-600 hover:bg-indigo-700 font-medium text-sm cursor-pointer border-0 py-2.5 pl-8 pr-4;
margin-inline-start: -1rem;
margin-inline-end: 1rem;
}
```

*For abbr of required fields:*

```
abbr[title=required] {
@apply !text-red-500 no-underline;
}
Copy link

@JuanVqz JuanVqz Sep 3, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @loqimean! Thanks for your contribution!

I noticed that we can use the following snippet at the config/locales/simple_form.en.yml file and override the abbr props

    required:
      # text: "required"
      # mark: "*"
      html: '<abbr class="text-red-500 no-underline" title="required">*</abbr>'

image_2023-09-03_002728140

btw, you should add the simple_form.*.yml into the content array at the tailwind.config.js file

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh, nice idea, can you add it to the PR?)

Copy link

@JuanVqz JuanVqz Oct 11, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure will add it.

```

### Country Select

If you want to use the country select, you will need the
Expand Down Expand Up @@ -348,7 +392,6 @@ Collection inputs accept two other options beside collections:

* *label_method* => the label method to be applied to the collection to retrieve the label (use this
instead of the `text_method` option in `collection_select`)

* *value_method* => the value method to be applied to the collection to retrieve the value

Those methods are useful to manipulate the given collection. Both of these options also accept
Expand Down Expand Up @@ -384,7 +427,6 @@ used to retrieve label/value attributes for the `option` tags. Besides that, you

* *group_method* => the method to be called on the given collection to generate the options for
each group (required)

* *group_label_method* => the label method to be applied on the given collection to retrieve the label
for the _optgroup_ (**Simple Form** will attempt to guess the best one the same way it does with
`:label_method`)
Expand Down Expand Up @@ -637,6 +679,7 @@ And use it in your views:
```ruby
f.input :money, as: :currency
```

Note, you may have to create the `app/inputs/` directory and restart your webserver.

You can also redefine existing **Simple Form** inputs by creating a new class with the same name. For
Expand Down Expand Up @@ -1073,7 +1116,7 @@ A cleaner method to create your views would be:
To use the number option on the input, first, tells to Simple Form the place where the components
will be:

``` ruby
```ruby
# config/initializers/simple_form.rb
Dir[Rails.root.join('lib/components/**/*.rb')].each { |f| require f }
```
Expand Down
9 changes: 6 additions & 3 deletions lib/generators/simple_form/install_generator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,13 @@ class InstallGenerator < Rails::Generators::Base
class_option :template_engine, desc: 'Template engine to be invoked (erb, haml or slim).'
class_option :bootstrap, type: :boolean, desc: 'Add the Bootstrap 5 wrappers to the SimpleForm initializer.'
class_option :foundation, type: :boolean, desc: 'Add the Zurb Foundation 5 wrappers to the SimpleForm initializer.'
class_option :tailwindcss, type: :boolean, desc: 'Add the TailwindCSS 3 wrappers to the SimpleForm initializer.'

def info_bootstrap
return if options.bootstrap? || options.foundation?
puts "SimpleForm supports Bootstrap 5 and Zurb Foundation 5. If you want "\
return if options.bootstrap? || options.foundation? || options.tailwindcss?
puts "SimpleForm 3 supports Bootstrap, Zurb Foundation 5 and TailwindCSS 3. If you want "\
"a configuration that is compatible with one of these frameworks, then please " \
"re-run this generator with --bootstrap or --foundation as an option."
"re-run this generator with --bootstrap, --foundation or --tailwindcss as an option."
end

def copy_config
Expand All @@ -22,6 +23,8 @@ def copy_config
template "config/initializers/simple_form_bootstrap.rb"
elsif options[:foundation]
template "config/initializers/simple_form_foundation.rb"
elsif options[:tailwindcss]
template "config/initializers/simple_form_tailwindcss.rb"
end

directory 'config/locales'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
# frozen_string_literal: true

# Please do not make direct changes to this file!
# The generator is written by author named 'loqimean'
# All future development, tests, and organization should happen there.
# Background history: https://github.com/heartcombo/simple_form/issues/1561

# Uncomment this and change the path if necessary to include your own
# components.
# See https://github.com/heartcombo/simple_form#custom-components
# to know more about custom components.
# Dir[Rails.root.join('lib/components/**/*.rb')].each { |f| require f }

# Use this setup block to configure all options available in SimpleForm.
SimpleForm.setup do |config|
config.wrappers :vertical_form, tag: 'div', class: 'mb-3', error_class: 'form-group-invalid',
valid_class: 'form-group-valid' do |b|
b.use :html5
b.use :placeholder, class: 'text-gray-400'
b.optional :maxlength
b.optional :minlength
b.optional :pattern
b.optional :min_max
b.optional :readonly
b.use :label, class: 'ml-1 mb-2'
b.use :input,
class: 'mt-1 focus:ring-indigo-500 focus:border-indigo-500 ' \
'block w-full shadow-sm sm:text-sm border-gray-300 rounded-md',
error_class: 'border-red-400 is-invalid mb-1', valid_class: 'is-valid'
b.use :full_error, wrap_with: { tag: 'div', class: 'invalid-feedback text-xs text-red-400' }
b.use :hint, wrap_with: { tag: 'small', class: 'text-gray-400' }
end

# vertical input for radio buttons and check boxes
config.wrappers :vertical_collection, item_wrapper_class: 'form-check',
item_label_class: 'form-check-label',
tag: 'fieldset',
class: 'form-group mb-3',
error_class: 'form-group-invalid',
valid_class: 'form-group-valid' do |b|
b.use :html5
b.optional :readonly
b.wrapper :legend_tag, tag: 'legend', class: 'col-form-label pt-0' do |ba|
ba.use :label_text, class: 'ml-3 block text-sm font-medium text-gray-700'
end
b.use :input, class: 'focus:ring-indigo-500 h-4 w-4 text-indigo-600 border-gray-300 mr-2',
error_class: 'is-invalid border-red-400',
valid_class: 'is-valid'
b.use :full_error, wrap_with: { tag: 'div', class: 'invalid-feedback text-xs text-red-400' }
b.use :hint, wrap_with: { tag: 'small', class: 'text-gray-400' }
end

# vertical input for radio buttons and check boxes
# config.wrappers :vertical_check_boxes_collection, item_wrapper_class: 'form-check',
# item_label_class: 'form-check-label',
# tag: 'fieldset', class: 'form-group mb-3',
# error_class: 'form-group-invalid',
# valid_class: 'form-group-valid' do |b|
# b.use :html5
# b.optional :readonly
# b.wrapper :legend_tag, tag: 'legend', class: 'col-form-label pt-0' do |ba|
# ba.use :label_text, class: 'ml-3 block text-sm font-medium text-gray-700'
# end
# b.use :input, class: 'focus:ring-indigo-500 h-4 w-4 text-indigo-600 border-gray-300 rounded mr-2',
# error_class: 'is-invalid border-red-400',
# valid_class: 'is-valid'
# b.use :full_error, wrap_with: { tag: 'div', class: 'invalid-feedback text-xs text-red-400' }
# b.use :hint, wrap_with: { tag: 'small', class: 'text-gray-400' }
# end

# horizontal input for inline radio buttons and check boxes
config.wrappers :horizontal_collection_inline, item_wrapper_class: 'form-check form-check-inline',
item_label_class: 'form-check-label',
tag: 'div',
class: 'form-group flex flex-row-reverse w-fit mb-3',
error_class: 'form-group-invalid',
valid_class: 'form-group-valid' do |b|
b.use :html5
b.optional :readonly
b.use :label, class: 'pt-0 mb-2'
b.wrapper :grid_wrapper, tag: 'div' do |ba|
ba.use :input, class: 'focus:ring-indigo-500 h-4 w-4 text-indigo-600 border-gray-300 rounded mr-2',
error_class: 'is-invalid', valid_class: 'is-valid'
ba.use :full_error, wrap_with: { tag: 'div', class: 'invalid-feedback text-xs text-red-400' }
ba.use :hint, wrap_with: { tag: 'small', class: 'text-gray-400' }
end
end

# vertical multi select
config.wrappers :vertical_multi_select, tag: 'div', class: 'mb-3',
error_class: 'form-group-invalid',
valid_class: 'form-group-valid' do |b|
b.use :html5
b.optional :readonly
b.use :label, class: 'mb-2'
b.wrapper tag: 'div', class: 'flex flex-col md:flex-row gap-1 justify-between items-center' do |ba|
ba.use :input, class: 'w-full min-w-fit mt-1 focus:ring-indigo-500 focus:border-indigo-500 block shadow-sm sm:text-sm border-gray-300 rounded-md',
error_class: '!border-red-500',
valid_class: 'is-valid'
end
b.use :full_error, wrap_with: { tag: 'div', class: 'invalid-feedback text-xs text-red-400' }
b.use :hint, wrap_with: { tag: 'small', class: 'text-gray-400' }
end

# vertical input for boolean
config.wrappers :vertical_boolean, tag: 'fieldset', class: 'mb-3',
error_class: 'form-group-invalid',
valid_class: 'form-group-valid' do |b|
b.use :html5
b.optional :readonly
b.wrapper :form_check_wrapper, tag: 'div' do |bb|
bb.use :input, class: 'focus:ring-indigo-500 h-4 w-4 text-indigo-600 border-gray-300 rounded mr-2',
error_class: '!border-red-500',
valid_class: 'is-valid'
bb.use :label, class: 'mb-2'
end
b.use :full_error, wrap_with: { tag: 'div', class: 'invalid-feedback text-xs text-red-400' }
b.use :hint, wrap_with: { tag: 'small', class: 'text-gray-400' }
end

# vertical file input
config.wrappers :vertical_file, tag: 'div',
class: 'form-group',
error_class: 'form-group-invalid',
valid_class: 'form-group-valid' do |b|
b.use :html5
b.use :placeholder
b.optional :maxlength
b.optional :minlength
b.optional :readonly
b.use :label, class: 'mb-2'
b.use :input, class: 'w-full min-w-fit mt-1 focus:ring-indigo-500 focus:border-indigo-500 block shadow-sm sm:text-sm border-gray-300 rounded-md border',
error_class: 'border-red-400 is-invalid mb-1',
valid_class: 'is-valid'
b.use :full_error, wrap_with: { tag: 'div', class: 'invalid-feedback text-xs text-red-400' }
b.use :hint, wrap_with: { tag: 'small', class: 'text-gray-400' }
end

# Custom wrappers for input types. This should be a hash containing an input
# type as key and the wrapper that will be used for all inputs with specified type.
config.wrapper_mappings = {
boolean: :vertical_boolean,
check_boxes: :vertical_collection,
date: :vertical_multi_select,
file: :vertical_file,
datetime: :vertical_multi_select,
radio_buttons: :vertical_collection,
time: :vertical_multi_select
}

# How the label text should be generated altogether with the required text.
config.label_text = ->(label, required, _explicit_label) { "#{label} #{required}" }

# CSS class for buttons
config.button_class = 'rounded-lg py-3 px-5 bg-blue-600 text-white inline-block font-medium cursor-pointer mb-1'

# Set this to div to make the checkbox and radio properly work
# otherwise simple_form adds a label tag instead of a div around
# the nested label
config.item_wrapper_tag = :div

# CSS class to add for error notification helper.
config.error_notification_class = 'bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded'

# Method used to tidy up errors. Specify any Rails Array method.
# :first lists the first message for each field.
# :to_sentence to list all errors for each field.
config.error_method = :to_sentence

# The default wrapper to be used by the FormBuilder.
config.default_wrapper = :vertical_form

# add validation classes to `input_field`
config.input_field_error_class = 'is-invalid'
config.input_field_valid_class = 'is-valid'
end
3 changes: 2 additions & 1 deletion lib/simple_form/tags.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ module CollectionExtensions
def render_collection
item_wrapper_tag = @options.fetch(:item_wrapper_tag, :span)
item_wrapper_class = @options[:item_wrapper_class]
item_wrapper_data = @options[:item_wrapper_data]

@collection.map do |item|
value = value_for_collection(item, @value_method)
Expand All @@ -22,7 +23,7 @@ def render_collection
rendered_item = @template_object.label(@object_name, sanitize_attribute_name(value), rendered_item, label_options)
end

item_wrapper_tag ? @template_object.content_tag(item_wrapper_tag, rendered_item, class: item_wrapper_class) : rendered_item
item_wrapper_tag ? @template_object.content_tag(item_wrapper_tag, rendered_item, class: item_wrapper_class, data: item_wrapper_data) : rendered_item
end.join.html_safe
end

Expand Down
8 changes: 8 additions & 0 deletions test/generators/simple_form_generator_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,14 @@ class SimpleFormGeneratorTest < Rails::Generators::TestCase
/config\.default_wrapper = :vertical_form/, /config\.item_wrapper_tag = :div/
end

test 'generates the simple_form initializer with the tailwindcss wrappers' do
run_generator %w[--tailwindcss]
assert_file 'config/initializers/simple_form.rb',
/config\.default_wrapper = :default/, /config\.boolean_style = :nested/
assert_file 'config/initializers/simple_form_tailwindcss.rb', /config\.wrappers :vertical_form/,
/config\.default_wrapper = :vertical_form/, /config\.item_wrapper_tag = :div/
end

%w[erb haml slim].each do |engine|
test "generates the scaffold template when using #{engine}" do
run_generator ['-e', engine]
Expand Down