diff --git a/README.md b/README.md index 97dc60d07..b129c8977 100644 --- a/README.md +++ b/README.md @@ -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; +} +``` + ### Country Select If you want to use the country select, you will need the @@ -359,7 +403,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 @@ -395,7 +438,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`) @@ -650,6 +692,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 @@ -1086,7 +1129,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 } ``` diff --git a/lib/generators/simple_form/install_generator.rb b/lib/generators/simple_form/install_generator.rb index bf7ca0653..ecfe54bd2 100644 --- a/lib/generators/simple_form/install_generator.rb +++ b/lib/generators/simple_form/install_generator.rb @@ -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 @@ -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' diff --git a/lib/generators/simple_form/templates/config/initializers/simple_form_tailwindcss.rb b/lib/generators/simple_form/templates/config/initializers/simple_form_tailwindcss.rb new file mode 100644 index 000000000..d0f643e9f --- /dev/null +++ b/lib/generators/simple_form/templates/config/initializers/simple_form_tailwindcss.rb @@ -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 mb-3', + 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 diff --git a/lib/simple_form/tags.rb b/lib/simple_form/tags.rb index 2d779b0dd..13c26e992 100644 --- a/lib/simple_form/tags.rb +++ b/lib/simple_form/tags.rb @@ -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) @@ -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 diff --git a/test/generators/simple_form_generator_test.rb b/test/generators/simple_form_generator_test.rb index abb526316..f15e2291f 100644 --- a/test/generators/simple_form_generator_test.rb +++ b/test/generators/simple_form_generator_test.rb @@ -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]