├── lib ├── simple_form │ └── tailwind │ │ ├── version.rb │ │ ├── inputs │ │ ├── append_string_input.rb │ │ ├── prepend_string_input.rb │ │ ├── password_input.rb │ │ └── string_input.rb │ │ ├── overwrite_class_with_error_or_valid_class.rb │ │ ├── form_builder.rb │ │ └── error_notification.rb ├── simple_form_tailwind_css.rb └── generators │ └── simple_form │ └── tailwind │ ├── install_generator.rb │ └── templates │ └── simple_form.rb ├── docs └── images │ ├── components │ ├── append.png │ ├── default.png │ ├── prepend.png │ ├── corner_hint.png │ └── default_with_error.png │ ├── error_notification_red.png │ └── error_notification_blue.png ├── Rakefile ├── .gitignore ├── Gemfile ├── LICENSE.txt ├── simple_form_tailwind_css.gemspec └── README.md /lib/simple_form/tailwind/version.rb: -------------------------------------------------------------------------------- 1 | module SimpleForm 2 | module Tailwind 3 | VERSION = "1.0.0" 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /docs/images/components/append.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abevoelker/simple_form_tailwind_css/HEAD/docs/images/components/append.png -------------------------------------------------------------------------------- /docs/images/components/default.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abevoelker/simple_form_tailwind_css/HEAD/docs/images/components/default.png -------------------------------------------------------------------------------- /docs/images/components/prepend.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abevoelker/simple_form_tailwind_css/HEAD/docs/images/components/prepend.png -------------------------------------------------------------------------------- /docs/images/components/corner_hint.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abevoelker/simple_form_tailwind_css/HEAD/docs/images/components/corner_hint.png -------------------------------------------------------------------------------- /docs/images/error_notification_red.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abevoelker/simple_form_tailwind_css/HEAD/docs/images/error_notification_red.png -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require "rspec/core/rake_task" 3 | 4 | RSpec::Core::RakeTask.new(:spec) 5 | 6 | task :default => :spec 7 | -------------------------------------------------------------------------------- /docs/images/error_notification_blue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abevoelker/simple_form_tailwind_css/HEAD/docs/images/error_notification_blue.png -------------------------------------------------------------------------------- /lib/simple_form_tailwind_css.rb: -------------------------------------------------------------------------------- 1 | require "heroicon" 2 | require "simple_form/tailwind/form_builder" 3 | require "simple_form/tailwind/error_notification" 4 | -------------------------------------------------------------------------------- /docs/images/components/default_with_error.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abevoelker/simple_form_tailwind_css/HEAD/docs/images/components/default_with_error.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /_yardoc/ 4 | /coverage/ 5 | /doc/ 6 | /pkg/ 7 | /spec/reports/ 8 | /spec/tmp 9 | /tmp/ 10 | Gemfile.lock 11 | 12 | # rspec failure tracking 13 | .rspec_status 14 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | git_source(:github) {|repo_name| "https://github.com/#{repo_name}" } 4 | 5 | # Specify your gem's dependencies in simple_form_tailwind.gemspec 6 | gemspec 7 | 8 | gem "rake", "~> 10.0" 9 | 10 | group :test do 11 | gem "rspec", "~> 3.0" 12 | gem "generator_spec" 13 | gem "timecop" 14 | gem "pry" 15 | end 16 | -------------------------------------------------------------------------------- /lib/generators/simple_form/tailwind/install_generator.rb: -------------------------------------------------------------------------------- 1 | require "rails/generators" 2 | 3 | module SimpleForm::Tailwind 4 | module Generators # :nodoc: 5 | class InstallGenerator < ::Rails::Generators::Base # :nodoc: 6 | desc "Creates SimpleForm initializer using default Tailwind components" 7 | 8 | def self.default_generator_root 9 | File.dirname(__FILE__) 10 | end 11 | 12 | def copy_initializer 13 | copy_file "simple_form.rb", "config/initializers/simple_form.rb" 14 | end 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/simple_form/tailwind/inputs/append_string_input.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require "simple_form/inputs/string_input" 3 | require "simple_form/tailwind/overwrite_class_with_error_or_valid_class" 4 | 5 | module SimpleForm 6 | module Tailwind 7 | module Inputs 8 | class AppendStringInput < SimpleForm::Inputs::StringInput 9 | include SimpleForm::Tailwind::OverwriteClassWithErrorOrValidClass 10 | 11 | def input(*args, &blk) 12 | input_html_options[:type] ||= "text" 13 | 14 | super 15 | end 16 | 17 | def append(wrapper_options = nil) 18 | template.content_tag(:span, options[:append], class: "inline-flex items-center px-3 rounded-r-md border border-l-0 border-gray-300 bg-gray-50 text-gray-500 sm:text-sm") 19 | end 20 | end 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/simple_form/tailwind/inputs/prepend_string_input.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require "simple_form/inputs/string_input" 3 | require "simple_form/tailwind/overwrite_class_with_error_or_valid_class" 4 | 5 | module SimpleForm 6 | module Tailwind 7 | module Inputs 8 | class PrependStringInput < SimpleForm::Inputs::StringInput 9 | include SimpleForm::Tailwind::OverwriteClassWithErrorOrValidClass 10 | 11 | def input(*args, &blk) 12 | input_html_options[:type] ||= "text" 13 | 14 | super 15 | end 16 | 17 | def prepend(wrapper_options = nil) 18 | template.content_tag(:span, options[:prepend], class: "inline-flex items-center px-3 rounded-l-md border border-r-0 border-gray-300 bg-gray-50 text-gray-500 sm:text-sm") 19 | end 20 | end 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/simple_form/tailwind/inputs/password_input.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | module SimpleForm 3 | module Tailwind 4 | module Inputs 5 | class PasswordInput < SimpleForm::Inputs::PasswordInput 6 | include SimpleForm::Tailwind::OverwriteClassWithErrorOrValidClass 7 | 8 | def input(*args, &blk) 9 | if has_errors? 10 | template.content_tag(:div, ( 11 | super + ( 12 | template.content_tag( 13 | :div, 14 | template.heroicon("exclamation-circle", options: { class: "h-5 w-5 text-red-500" }), 15 | class: "absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none" 16 | ) 17 | ) 18 | ), class: "mt-1 relative rounded-md shadow-sm") 19 | else 20 | template.content_tag(:div, super, class: "mt-1") 21 | end 22 | end 23 | end 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/simple_form/tailwind/overwrite_class_with_error_or_valid_class.rb: -------------------------------------------------------------------------------- 1 | module SimpleForm 2 | module Tailwind 3 | # This module can be mixed in to inputs to change the default 4 | # :error_class and :valid_class option behavior to *overwrite* the existing 5 | # :class value when there's an error or the model is valid, instead of 6 | # *adding* these classes to the :class value. 7 | module OverwriteClassWithErrorOrValidClass 8 | def set_input_classes(wrapper_options) 9 | wrapper_options = wrapper_options.dup 10 | error_class = wrapper_options.delete(:error_class) 11 | valid_class = wrapper_options.delete(:valid_class) 12 | 13 | if error_class.present? && has_errors? 14 | wrapper_options[:class] = error_class 15 | end 16 | 17 | if valid_class.present? && valid? 18 | wrapper_options[:class] = valid_class 19 | end 20 | 21 | wrapper_options 22 | end 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/simple_form/tailwind/inputs/string_input.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require "simple_form/inputs/string_input" 3 | require "simple_form/tailwind/overwrite_class_with_error_or_valid_class" 4 | 5 | module SimpleForm 6 | module Tailwind 7 | module Inputs 8 | class StringInput < SimpleForm::Inputs::StringInput 9 | include SimpleForm::Tailwind::OverwriteClassWithErrorOrValidClass 10 | 11 | def input(*args, &blk) 12 | if has_errors? 13 | template.content_tag(:div, ( 14 | super + ( 15 | template.content_tag( 16 | :div, 17 | template.heroicon("exclamation-circle", options: { class: "h-5 w-5 text-red-500" }), 18 | class: "absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none" 19 | ) 20 | ) 21 | ), class: "mt-1 relative rounded-md shadow-sm") 22 | else 23 | template.content_tag(:div, super, class: "mt-1") 24 | end 25 | end 26 | end 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/simple_form/tailwind/form_builder.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require "simple_form/form_builder" 3 | require "simple_form/tailwind/inputs/append_string_input" 4 | require "simple_form/tailwind/inputs/password_input" 5 | require "simple_form/tailwind/inputs/prepend_string_input" 6 | require "simple_form/tailwind/inputs/string_input" 7 | 8 | module SimpleForm 9 | module Tailwind 10 | class FormBuilder < SimpleForm::FormBuilder 11 | map_type :string, :email, :search, :tel, :url, :uuid, :citext, to: SimpleForm::Tailwind::Inputs::StringInput 12 | map_type :password, to: SimpleForm::Tailwind::Inputs::PasswordInput 13 | map_type :prepend_string, to: SimpleForm::Tailwind::Inputs::PrependStringInput 14 | map_type :append_string, to: SimpleForm::Tailwind::Inputs::AppendStringInput 15 | 16 | def error_notification(options = {}) 17 | SimpleForm::Tailwind::ErrorNotification.new(self, options).render 18 | end 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2021 Abe Voelker 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /lib/simple_form/tailwind/error_notification.rb: -------------------------------------------------------------------------------- 1 | module SimpleForm 2 | module Tailwind 3 | class ErrorNotification < SimpleForm::ErrorNotification 4 | def render 5 | if @options[:color] 6 | warn "WARNING SimpleForm::Tailwind's :color error_notification option has " \ 7 | "been removed due to incompatibility with Tailwind's JIT. " \ 8 | "Please replace with the new :icon_classes, :message_classes, and " \ 9 | ":border_classes options (see README for an example)." 10 | end 11 | 12 | if has_errors? 13 | icon_classes = @options.fetch(:icon_classes, "h-5 w-5 text-red-400") 14 | message_classes = @options.fetch(:message_classes, "text-sm text-red-700") 15 | border_classes = @options.fetch(:border_classes, "bg-red-50 border-l-4 border-red-400 p-4") 16 | icon = @options.fetch(:icon, "x-circle") 17 | 18 | template.content_tag( 19 | :div, 20 | ( 21 | template.content_tag( 22 | :div, 23 | ( 24 | template.content_tag( 25 | :div, 26 | template.heroicon(icon, options: { class: icon_classes }), 27 | class: "flex-shrink-0" 28 | ) + template.content_tag( 29 | :div, 30 | template.content_tag( 31 | :p, 32 | error_message, 33 | class: message_classes 34 | ), 35 | class: "ml-3" 36 | ) 37 | ), 38 | class: "flex" 39 | ) 40 | ), 41 | class: border_classes 42 | ) 43 | end 44 | end 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /simple_form_tailwind_css.gemspec: -------------------------------------------------------------------------------- 1 | 2 | lib = File.expand_path("../lib", __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require "simple_form/tailwind/version" 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = "simple_form_tailwind_css" 8 | spec.version = SimpleForm::Tailwind::VERSION 9 | spec.authors = ["Abe Voelker"] 10 | spec.email = ["_@abevoelker.com"] 11 | 12 | spec.summary = %q{Tailwind components for Simple Form} 13 | #spec.description = %q{TODO: Write a longer description or delete this line.} 14 | spec.homepage = "https://github.com/abevoelker/simple_form_tailwind_css" 15 | spec.license = "MIT" 16 | 17 | # Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host' 18 | # to allow pushing to a single host or delete this section to allow pushing to any host. 19 | if spec.respond_to?(:metadata) 20 | #spec.metadata["allowed_push_host"] = "TODO: Set to 'http://mygemserver.com'" 21 | 22 | spec.metadata["homepage_uri"] = spec.homepage 23 | spec.metadata["source_code_uri"] = spec.homepage 24 | #spec.metadata["changelog_uri"] = "TODO: Put your gem's CHANGELOG.md URL here." 25 | else 26 | raise "RubyGems 2.0 or newer is required to protect against " \ 27 | "public gem pushes." 28 | end 29 | 30 | # Specify which files should be added to the gem when it is released. 31 | # The `git ls-files -z` loads the files in the RubyGem that have been added into git. 32 | spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do 33 | `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) } 34 | end 35 | spec.bindir = "exe" 36 | spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } 37 | spec.require_paths = ["lib"] 38 | spec.required_ruby_version = ">= 2.1.0" 39 | 40 | spec.add_dependency "simple_form" 41 | spec.add_dependency "heroicon" 42 | end 43 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SimpleForm::Tailwind 2 | 3 | Tailwind components for [Simple Form][] 4 | 5 | ## Prerequisites 6 | 7 | You should have these installed first: 8 | 9 | * [Simple Form gem][Simple Form] 10 | * [Tailwind][] 11 | * We're agnostic about install method; the Rails [tailwindcss-rails gem][] is one method 12 | * [Heroicon gem][] 13 | * Be sure to [run the install generator][heroicon generator] if it's a new dependency to your project 14 | 15 | [Simple Form]: https://github.com/heartcombo/simple_form 16 | [Tailwind]: https://tailwindcss.com/ 17 | [tailwindcss-rails gem]: https://github.com/rails/tailwindcss-rails 18 | [Heroicon gem]: https://github.com/bharget/heroicon 19 | [heroicon generator]: https://github.com/bharget/heroicon#installation 20 | 21 | ## Installation 22 | 23 | Add to your application's Gemfile: 24 | 25 | ```ruby 26 | gem "simple_form_tailwind_css" 27 | ``` 28 | 29 | And then execute: 30 | 31 | ``` 32 | $ bundle install 33 | ``` 34 | 35 | Next, overwrite your Simple Form initializer with ours: 36 | 37 | ``` 38 | $ rails g simple_form:tailwind:install 39 | ``` 40 | 41 | Finally, for Tailwind 3 we need to modify `tailwind.config.js` to add an environment 42 | variable that tells Tailwind where the gem's Ruby source is so that classes used by 43 | the gem aren't pruned by Tailwind's JIT: 44 | 45 | ```diff 46 | --- a/tailwind.config.js 47 | +++ b/tailwind.config.js 48 | @@ -4,6 +4,7 @@ const colors = require('tailwindcss/colors') 49 | /** @type {import('tailwindcss').Config} */ 50 | module.exports = { 51 | content: [ 52 | + `${process.env.SIMPLE_FORM_TAILWIND_DIR}/**/*.rb`, 53 | './app/views/**/*.{html,erb,haml}', 54 | './app/helpers/**/*.rb', 55 | './app/javascript/**/*.{js,jsx,ts,tsx,vue}', 56 | ``` 57 | 58 | We're not done yet - jump down to the 59 | [Tailwind JIT configuration](#tailwind-jit-configuration) section to read 60 | approaches on how to pass in this variable to complete setup. 61 | 62 | ## Usage 63 | 64 | Here's an example form demonstrating usage: 65 | 66 | ```erb 67 | <%= simple_form_for(@foo, builder: SimpleForm::Tailwind::FormBuilder) do |f| %> 68 | <%= f.error_notification %> 69 | <%= f.input :name, autocomplete: "name", placeholder: "Alex Smith", label: "Display name" %> 70 | <%= f.input :email, autocomplete: "email", placeholder: "asmith@example.com", label: "Email address" %> 71 |