├── .gitignore ├── .rspec ├── .rubocop.yml ├── .rubocop_todo.yml ├── .travis.yml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── Gemfile ├── LICENSE ├── README.md ├── Rakefile ├── foundation_rails_helper.gemspec ├── lib ├── foundation_rails_helper.rb ├── foundation_rails_helper │ ├── action_view_extension.rb │ ├── configuration.rb │ ├── flash_helper.rb │ ├── form_builder.rb │ ├── size_class_calculator.rb │ └── version.rb └── railtie.rb └── spec ├── .rubocop.yml ├── foundation_rails_helper ├── configuration_spec.rb ├── flash_helper_spec.rb └── form_builder_spec.rb ├── spec_helper.rb └── support ├── .rubocop.yml ├── classes ├── author.rb ├── book.rb └── genre.rb └── mock_rails.rb /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | .bundle 4 | .config 5 | .yardoc 6 | Gemfile.lock 7 | InstalledFiles 8 | _yardoc 9 | coverage 10 | doc/ 11 | lib/bundler/man 12 | pkg 13 | rdoc 14 | spec/reports 15 | test/tmp 16 | test/version_tmp 17 | tmp 18 | .rbenv-gemsets 19 | vendor/bundle/* 20 | .ruby-version 21 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --backtrace 3 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | inherit_from: .rubocop_todo.yml 2 | 3 | AllCops: 4 | TargetRubyVersion: 2.3 5 | Include: 6 | - Rakefile 7 | - Gemfile 8 | - '*.gemspec' 9 | Exclude: 10 | - 'vendor/**/*' 11 | 12 | Documentation: 13 | Enabled: false 14 | 15 | Style/VariableNumber: 16 | EnforcedStyle: 'snake_case' 17 | -------------------------------------------------------------------------------- /.rubocop_todo.yml: -------------------------------------------------------------------------------- 1 | # This configuration was generated by 2 | # `rubocop --auto-gen-config` 3 | # on 2017-01-13 13:27:28 -0800 using RuboCop version 0.44.1. 4 | # The point is for the user to remove these configuration records 5 | # one by one as the offenses are removed from the code base. 6 | # Note that changes in the inspected code, or installation of new 7 | # versions of RuboCop, may require this file to be generated again. 8 | 9 | # Offense count: 1 10 | Metrics/AbcSize: 11 | Max: 17 12 | 13 | # Offense count: 1 14 | # Configuration parameters: CountComments. 15 | Metrics/ClassLength: 16 | Max: 183 17 | 18 | # Offense count: 1 19 | Metrics/CyclomaticComplexity: 20 | Max: 7 21 | 22 | # Offense count: 1 23 | # Configuration parameters: CountComments. 24 | Metrics/MethodLength: 25 | Max: 13 26 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | 3 | cache: bundler 4 | 5 | branches: 6 | only: 7 | - master 8 | rvm: 9 | - 2.3.8 10 | - 2.4.10 11 | - 2.5.8 12 | - 2.6.6 13 | - 2.7.2 14 | env: 15 | - "RAILS_VERSION=4.2.0" 16 | - "RAILS_VERSION=5.0.0" 17 | - "RAILS_VERSION=5.2.0" 18 | - "RAILS_VERSION=6.0.0" 19 | - "RAILS_VERSION=6.1.0" 20 | jobs: 21 | exclude: 22 | - rvm: 2.4.10 23 | env: "RAILS_VERSION=6.1.0" 24 | - rvm: 2.3.8 25 | env: "RAILS_VERSION=6.1.0" 26 | - rvm: 2.7.2 27 | env: "RAILS_VERSION=6.0.0" 28 | - rvm: 2.4.10 29 | env: "RAILS_VERSION=6.0.0" 30 | - rvm: 2.3.8 31 | env: "RAILS_VERSION=6.0.0" 32 | - rvm: 2.7.2 33 | env: "RAILS_VERSION=5.2.0" 34 | - rvm: 2.7.2 35 | env: "RAILS_VERSION=5.0.0" 36 | - rvm: 2.7.2 37 | env: "RAILS_VERSION=4.2.0" 38 | - rvm: 2.6.6 39 | env: "RAILS_VERSION=4.2.0" 40 | - rvm: 2.5.8 41 | env: "RAILS_VERSION=4.2.0" 42 | - rvm: 2.4.10 43 | env: "RAILS_VERSION=4.2.0" 44 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## Version 3.0.0 2 | * Added Foundation 6 support 3 | * Added auto_labels config 4 | 5 | ## Version 2.0 6 | This will be used for Foundation 5 support 7 | 8 | ### Breaking changes: 9 | * Dropped support for Ruby 1.9.3 10 | * display_flash_messages now requires the key_matching hash to be prefixed with the keyword argument :key_matching 11 | 12 | ### Features: 13 | * Add Rubocop code style linting 14 | 15 | ## Version 1.2.2 16 | * Fix Rails 5 deprecation warnings about alias_method_chain 17 | * Allow Capybara to be upgraded beyond 2.6.x 18 | 19 | ## Version 1.2.1 20 | * Lock Capybara at 2.6.x 21 | 22 | ## Version 1.2.0 23 | * Allow Rails 5 to be used 24 | 25 | ## Version 1.1.0 26 | * Form Helper: [Prefix and 27 | Postfix](http://foundation.zurb.com/sites/docs/v/5.5.3/components/forms.html#pre-postfix-labels-amp-actions) are now supported (PR #104 thanks djkz) 28 | * Flash Helper: a list of keys to be ignored can be specified - e.g. keys which are only used internally to pass data, not user-viewable messages(fix for #98) 29 | * FIX: Hints are added as span elements (PR #96 thanks collimarco) 30 | * FIX: Labels and fields don't have empty class attributes or repeated error classes 31 | (thanks collimarco) 32 | * FIX: Radio buttons don't have the `label="false"` on them when `label: 33 | false` is set (PR #107 thanks frenkel) 34 | 35 | ## Version 1.0.0 36 | 37 | * Released Feb 03rd 2015 38 | * Added configuration option to set button class in an initializer 39 | * Updated to be compatible with Foundation 5.2.2 40 | * Bugfixes 41 | 42 | ## Version 0.5.0 43 | 44 | * Released Oct 10th 2014 45 | * Bugfixes 46 | 47 | ## Version 0.4 48 | 49 | * Not released 50 | * Compatibility with Rails 4 51 | * Bugfixes 52 | 53 | ## Version 0.3 54 | 55 | * Not released 56 | * Mostly bugfixes 57 | 58 | ## Version 0.2.1 59 | 60 | * First production release (Jan 14th, 2012) 61 | 62 | ## Version 0.1.rc 63 | 64 | * initial release candidate (Jan 14th, 2012) 65 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ### We love pull requests. Here's a quick guide. 4 | 5 | Fork, then clone the repo: 6 | ``` 7 | git clone git@github.com:your-username/foundation_rails_helper.git 8 | ``` 9 | 10 | Set up your machine: 11 | ``` 12 | git checkout -b your-branch-name 13 | bundle 14 | ``` 15 | 16 | Make sure the tests pass: 17 | ``` 18 | rake 19 | ``` 20 | 21 | Make your change. Add tests for your change. Make the tests pass: 22 | ``` 23 | rake 24 | ``` 25 | 26 | Push to your fork and [submit a pull request][pr]. 27 | ``` 28 | git push origin your-branch-name 29 | ``` 30 | 31 | [pr]: https://github.com/sgruhier/foundation_rails_helper/compare/ 32 | 33 | At this point you're waiting on us. We like to at least comment on pull requests within three business days (and, typically, one business day). We may suggest some changes or improvements or alternatives. 34 | 35 | Some things that will increase the chance that your pull request is accepted: 36 | 37 | * Write tests. 38 | * Write a [good commit message][commit]. 39 | 40 | [commit]: http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html 41 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | source 'https://rubygems.org' 3 | 4 | # Specify your gem's dependencies in foundation_rails_helper.gemspec 5 | gemspec 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 Sébastien Gruhier 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Foundation Rails Helper [![Build Status](https://secure.travis-ci.org/sgruhier/foundation_rails_helper.png)](http://travis-ci.org/sgruhier/foundation_rails_helper) 2 | 3 | Gem for Rails 4.1+ applications that use the excellent [Zurb Foundation framework](https://github.com/zurb/foundation-rails). 4 | 5 | Includes: 6 | 7 | * A custom FormBuilder that generates a form using the Foundation framework classes. It replaces the current `form_for`, so there is no need to change your Rails code. Error messages are properly displayed. 8 | 9 | * A `display_flash_messages` helper method that uses Zurb Foundation Callout UI. 10 | 11 | ## Installation 12 | 13 | Add this line to your application's Gemfile: 14 | 15 | ```ruby 16 | gem 'foundation-rails', '~> 6.0' # required 17 | gem 'foundation_rails_helper', '~> 3.0' 18 | ``` 19 | 20 | And then execute: 21 | 22 | ```bash 23 | $ bundle 24 | ``` 25 | 26 | ## Compatibility 27 | 28 | * Only Rails 4.1/4.2/5/6, and Foundation 6 are fully supported 29 | * Some features may work with Foundation 5 and older, but results may vary, and markup which exists only for those versions will be gradually removed 30 | * Legacy branches exist for Rails 3, 4.0, and Foundation 5 (see the rails3, rails4.0, and foundation-5 branches). These are not actively supported, and fixes are not retroactively applied, but pull requests are welcome. 31 | * We test against ruby versions 2.1 and up. This gem may still work fine on 1.9.3, but your mileage may vary 32 | 33 | 34 | ## Screenshots 35 | 36 | ### Forms 37 | A classic devise sign up view will look like this: 38 | 39 | ```erb 40 | <%= form_for(resource, :as => resource_name, :url => registration_path(resource_name)) do |f| %> 41 | <%= f.email_field :email %> 42 | <%= f.password_field :password %> 43 | <%= f.password_field :password_confirmation %> 44 | 45 | <%= f.submit %> 46 | <% end %> 47 | ``` 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 61 | 64 | 65 | 66 |
FormForm with errors
59 | 60 | 62 | 63 |
67 | 68 | ### Flash messages 69 | 70 | ![Flash-message](https://cloud.githubusercontent.com/assets/1400414/18522256/3d13c97e-7a64-11e6-9ee2-33adc93cd573.png "Flash-message") 71 | 72 | ## Usage 73 | 74 | ### Flash Messages 75 | 76 | To use the built in flash helper, add `<%= display_flash_messages %>` to your layout file (eg. *app/views/layouts/application.html.erb*). 77 | 78 | ### form_for 79 | 80 | Form_for wraps the standard rails form_for helper. 81 | 82 | ```erb 83 | <%= form_for @user do |f| %> 84 | ... 85 | <% end %> 86 | ``` 87 | 88 | generates: 89 | 90 | ```html 91 |
92 | ... 93 |
94 | ``` 95 | 96 | ### text_field and Field Helpers 97 | 98 | Field helpers add a label element and an input of the proper type. 99 | 100 | ```ruby 101 | f.text_field :name 102 | ``` 103 | 104 | generates: 105 | 106 | ```html 107 | 108 | 109 | ``` 110 | 111 | Preventing the generation of labels can be accomplished three ways. To disable on a form element: 112 | ```ruby 113 | f.text_field :name, label: false 114 | ``` 115 | For all form elements, add the option: `auto_labels: false` to the form helper. To disable for all forms in you project, use the `auto_labels` config option, see the Configuration section for more information. 116 | 117 | Change the label text and add a class on the label: 118 | 119 | ```ruby 120 | f.text_field :name, label: 'Nombre', label_options: { class: 'large' } 121 | ``` 122 | 123 | If the help_text option is specified 124 | 125 | ```ruby 126 | f.text_field :name, help_text: "I'm a text field" 127 | ``` 128 | 129 | an additional p element will be added after the input element: 130 | 131 | ```html 132 |

I'm a text field

133 | ``` 134 | 135 | ### Submit Button 136 | 137 | The 'submit' helper wraps the rails helper and sets the class attribute to "success button" by default. 138 | 139 | ```ruby 140 | f.submit 141 | ``` 142 | 143 | generates: 144 | 145 | ```html 146 | 147 | ``` 148 | 149 | Specify the class option to override the default classes. 150 | 151 | ### Errors 152 | 153 | On error, 154 | 155 | ```ruby 156 | f.email_field :email 157 | ``` 158 | 159 | generates: 160 | 161 | ```html 162 | 163 | 164 | can't be blank 165 | ``` 166 | 167 | The class attribute of the 'small' element will mirror the class attribute of the 'input' element. 168 | 169 | If the `html_safe_errors: true` option is specified on a field, then any HTML you may have embedded in a custom error string will be displayed with the html_safe option. 170 | 171 | ### Prefix and Postfix 172 | Simple prefix and postfix span elements can be added beside inputs. 173 | ```ruby 174 | f.text_field :name, prefix { value: 'foo', small: 2, large: 3 } 175 | ``` 176 | generates 177 | ```html 178 |
179 |
180 | foo 181 |
182 |
183 | 184 |
185 |
186 | ``` 187 | 188 | 189 | ## Configuration 190 | Add an initializer file to your Rails app: *config/initializers/foundation_rails_helper.rb* 191 | containing the following block: 192 | 193 | ```ruby 194 | FoundationRailsHelper.configure do |config| 195 | # your options here 196 | end 197 | ``` 198 | 199 | ### Submit Button Class 200 | To use a different class for the [submit button](https://github.com/sgruhier/foundation_rails_helper#submit-button) used in `form_for`, add a config named **button_class**: 201 | ```ruby 202 | # Default: 'success button' 203 | config.button_class = 'large hollow secondary button' 204 | ``` 205 | 206 | Please note, the button class can still be overridden by an options hash. 207 | 208 | ### Ignored Flash Keys 209 | The flash helper assumes all flash entries are user-viewable messages. 210 | To exclude flash entries which are used for storing state 211 | (e.g. [Devise's `:timedout` flash](https://github.com/plataformatec/devise/issues/1777)) 212 | you can specify a blacklist of keys to ignore with the **ignored_flash_keys** config option: 213 | ```ruby 214 | # Default: [] 215 | config.ignored_flash_keys = [:timedout] 216 | ``` 217 | 218 | ### Auto Labels 219 | If you prefer to not have the form builder automatically generate labels, set `auto_labels` to false. 220 | ```ruby 221 | # Default: true 222 | config.auto_labels = false 223 | ``` 224 | 225 | ## Contributing 226 | 227 | See the [CONTRIBUTING](CONTRIBUTING.md) file. 228 | 229 | ## Copyright 230 | 231 | Sébastien Gruhier (http://xilinus.com) - MIT LICENSE 232 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env rake 2 | # frozen_string_literal: true 3 | require 'bundler/gem_tasks' 4 | require 'rspec/core/rake_task' 5 | require 'rubocop/rake_task' 6 | 7 | RSpec::Core::RakeTask.new(:spec) 8 | RuboCop::RakeTask.new(:rubocop) 9 | task default: [:spec, :rubocop] 10 | -------------------------------------------------------------------------------- /foundation_rails_helper.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require File.expand_path('../lib/foundation_rails_helper/version', __FILE__) 3 | 4 | class Gem::Specification # rubocop:disable ClassAndModuleChildren 5 | def self.rails_gem_version 6 | # Allow different versions of the rails gems to be specified, for testing 7 | @rails_gem_version ||= 8 | ENV['RAILS_VERSION'] ? "~> #{ENV['RAILS_VERSION']}" : ['>= 4.1', '< 7.1'] 9 | end 10 | end 11 | 12 | Gem::Specification.new do |gem| 13 | gem.authors = ['Sebastien Gruhier'] 14 | gem.email = ['sebastien.gruhier@xilinus.com'] 15 | gem.description = 16 | 'Rails for zurb foundation CSS framework. Form builder, flash message, ...' 17 | gem.summary = 'Rails helpers for zurb foundation CSS framework' 18 | gem.homepage = 'http://github.com/sgruhier/foundation_rails_helper' 19 | 20 | gem.executables = 21 | `git ls-files -- bin/*`.split("\n").map { |f| File.basename(f) } 22 | gem.files = `git ls-files`.split("\n") 23 | gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") 24 | gem.name = 'foundation_rails_helper' 25 | gem.require_paths = %w(lib) 26 | gem.version = FoundationRailsHelper::VERSION 27 | gem.license = 'MIT' 28 | 29 | gem.add_dependency 'railties', Gem::Specification.rails_gem_version 30 | gem.add_dependency 'actionpack', Gem::Specification.rails_gem_version 31 | gem.add_dependency 'activemodel', Gem::Specification.rails_gem_version 32 | gem.add_dependency 'activesupport', Gem::Specification.rails_gem_version 33 | 34 | gem.add_development_dependency 'rspec-rails', '~> 3.1' 35 | gem.add_development_dependency 'mime-types', '~> 2' 36 | gem.add_development_dependency 'capybara', '~> 2.7' 37 | gem.add_development_dependency 'rubocop', '~> 0.44.1' 38 | end 39 | -------------------------------------------------------------------------------- /lib/foundation_rails_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require 'foundation_rails_helper/version' 3 | require 'foundation_rails_helper/configuration' 4 | require 'foundation_rails_helper/form_builder' 5 | require 'foundation_rails_helper/size_class_calculator' 6 | require 'foundation_rails_helper/flash_helper' 7 | require 'foundation_rails_helper/action_view_extension' 8 | ActiveSupport.on_load(:action_view) do 9 | include FoundationRailsHelper::FlashHelper 10 | end 11 | -------------------------------------------------------------------------------- /lib/foundation_rails_helper/action_view_extension.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | ActionView::Base.default_form_builder = FoundationRailsHelper::FormBuilder 3 | ActionView::Base.field_error_proc = proc do |html_tag, _instance_tag| 4 | html_tag 5 | end 6 | -------------------------------------------------------------------------------- /lib/foundation_rails_helper/configuration.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | module FoundationRailsHelper 3 | class << self 4 | attr_writer :configuration 5 | end 6 | 7 | def self.configuration 8 | @configuration ||= Configuration.new 9 | end 10 | 11 | def self.reset 12 | @configuration = Configuration.new 13 | end 14 | 15 | def self.configure 16 | yield(configuration) 17 | end 18 | 19 | class Configuration 20 | attr_accessor :button_class 21 | attr_accessor :ignored_flash_keys 22 | attr_accessor :auto_labels 23 | 24 | def initialize 25 | @button_class = 'success button' 26 | @ignored_flash_keys = [] 27 | @auto_labels = true 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /lib/foundation_rails_helper/flash_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require 'action_view/helpers' 3 | 4 | module FoundationRailsHelper 5 | module FlashHelper 6 | #
7 | # This is an alert box. 8 | # 11 | #
12 | DEFAULT_KEY_MATCHING = { 13 | alert: :alert, 14 | notice: :success, 15 | info: :info, 16 | secondary: :secondary, 17 | success: :success, 18 | error: :alert, 19 | warning: :warning, 20 | primary: :primary 21 | }.freeze 22 | 23 | # Displays the flash messages found in ActionDispatch's +flash+ hash using 24 | # Foundation's +callout+ component. 25 | # 26 | # Parameters: 27 | # * +closable+ - A boolean to determine whether the displayed flash messages 28 | # should be closable by the user. Defaults to true. 29 | # * +key_matching+ - A Hash of key/value pairs mapping flash keys to the 30 | # corresponding class to use for the callout box. 31 | def display_flash_messages(closable: true, key_matching: {}) 32 | key_matching = DEFAULT_KEY_MATCHING.merge(key_matching) 33 | key_matching.default = :primary 34 | 35 | capture do 36 | flash.each do |key, value| 37 | next if ignored_key?(key.to_sym) 38 | 39 | alert_class = key_matching[key.to_sym] 40 | concat alert_box(value, alert_class, closable) 41 | end 42 | end 43 | end 44 | 45 | private 46 | 47 | def alert_box(value, alert_class, closable) 48 | options = { class: "flash callout #{alert_class}" } 49 | options[:data] = { closable: '' } if closable 50 | content_tag(:div, options) do 51 | concat value 52 | concat close_link if closable 53 | end 54 | end 55 | 56 | def close_link 57 | button_tag( 58 | class: 'close-button', 59 | type: 'button', 60 | data: { close: '' }, 61 | aria: { label: 'Dismiss alert' } 62 | ) do 63 | content_tag(:span, '×'.html_safe, aria: { hidden: true }) 64 | end 65 | end 66 | 67 | def ignored_key?(key) 68 | FoundationRailsHelper.configuration.ignored_flash_keys.include?(key) 69 | end 70 | end 71 | end 72 | -------------------------------------------------------------------------------- /lib/foundation_rails_helper/form_builder.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require 'action_view/helpers' 3 | 4 | module FoundationRailsHelper 5 | class FormBuilder < ActionView::Helpers::FormBuilder 6 | include ActionView::Helpers::TagHelper 7 | include ActionView::Helpers::OutputSafetyHelper 8 | %w(file_field email_field text_field text_area telephone_field phone_field 9 | url_field number_field date_field datetime_field datetime_local_field 10 | month_field week_field time_field range_field search_field color_field) 11 | .each do |method_name| 12 | define_method(method_name) do |*args| 13 | attribute = args[0] 14 | options = args[1] || {} 15 | field(attribute, options) do |opts| 16 | super(attribute, opts) 17 | end 18 | end 19 | end 20 | 21 | def label(attribute, text = nil, options = {}) 22 | if error?(attribute) 23 | options[:class] ||= '' 24 | options[:class] += ' is-invalid-label' 25 | end 26 | 27 | super(attribute, (text || '').html_safe, options) 28 | end 29 | 30 | # rubocop:disable LineLength 31 | def check_box(attribute, options = {}, checked_value = '1', unchecked_value = '0') 32 | custom_label(attribute, options[:label], options[:label_options]) do 33 | options.delete(:label) 34 | options.delete(:label_options) 35 | super(attribute, options, checked_value, unchecked_value) 36 | end + error_and_help_text(attribute, options) 37 | end 38 | # rubocop:enable LineLength 39 | 40 | def radio_button(attribute, tag_value, options = {}) 41 | options[:label_options] ||= {} 42 | label_options = options.delete(:label_options).merge!(value: tag_value) 43 | label_text = options.delete(:label) 44 | l = label(attribute, label_text, label_options) unless label_text == false 45 | r = @template.radio_button(@object_name, attribute, tag_value, 46 | objectify_options(options)) 47 | 48 | "#{r}#{l}".html_safe 49 | end 50 | 51 | def password_field(attribute, options = {}) 52 | field attribute, options do |opts| 53 | super(attribute, opts.merge(autocomplete: :off)) 54 | end 55 | end 56 | 57 | def datetime_select(attribute, options = {}, html_options = {}) 58 | field attribute, options, html_options do |html_opts| 59 | super(attribute, options, html_opts.merge(autocomplete: :off)) 60 | end 61 | end 62 | 63 | def date_select(attribute, options = {}, html_options = {}) 64 | field attribute, options, html_options do |html_opts| 65 | super(attribute, options, html_opts.merge(autocomplete: :off)) 66 | end 67 | end 68 | 69 | # rubocop:disable LineLength 70 | def time_zone_select(attribute, priorities = nil, options = {}, html_options = {}) 71 | field attribute, options, html_options do |html_opts| 72 | super(attribute, priorities, options, 73 | html_opts.merge(autocomplete: :off)) 74 | end 75 | end 76 | # rubocop:enable LineLength 77 | 78 | def select(attribute, choices, options = {}, html_options = {}) 79 | field attribute, options, html_options do |html_opts| 80 | html_options[:autocomplete] ||= :off 81 | super(attribute, choices, options, html_opts) 82 | end 83 | end 84 | 85 | # rubocop:disable LineLength, ParameterLists 86 | def collection_select(attribute, collection, value_method, text_method, options = {}, html_options = {}) 87 | field attribute, options, html_options do |html_opts| 88 | html_options[:autocomplete] ||= :off 89 | super(attribute, collection, value_method, text_method, options, 90 | html_opts) 91 | end 92 | end 93 | 94 | def grouped_collection_select(attribute, collection, group_method, group_label_method, option_key_method, option_value_method, options = {}, html_options = {}) 95 | field attribute, options, html_options do |html_opts| 96 | html_options[:autocomplete] ||= :off 97 | super(attribute, collection, group_method, group_label_method, 98 | option_key_method, option_value_method, options, html_opts) 99 | end 100 | end 101 | # rubocop:enable LineLength, ParameterLists 102 | 103 | def autocomplete(attribute, url, options = {}) 104 | field attribute, options do |opts| 105 | opts.merge!(update_elements: opts[:update_elements], 106 | min_length: 0, value: object.send(attribute)) 107 | autocomplete_field(attribute, url, opts) 108 | end 109 | end 110 | 111 | def submit(value = nil, options = {}) 112 | options[:class] ||= FoundationRailsHelper.configuration.button_class 113 | super(value, options) 114 | end 115 | 116 | def error_for(attribute, options = {}) 117 | return unless error?(attribute) 118 | 119 | class_name = 'form-error is-visible' 120 | class_name += " #{options[:class]}" if options[:class] 121 | 122 | error_messages = object.errors[attribute].join(', ') 123 | error_messages = error_messages.html_safe if options[:html_safe_errors] 124 | content_tag(:small, error_messages, 125 | class: class_name.sub('is-invalid-input', '')) 126 | end 127 | 128 | private 129 | 130 | def error?(attribute) 131 | object.respond_to?(:errors) && !object.errors[attribute].blank? 132 | end 133 | 134 | def default_label_text(object, attribute) 135 | if object.class.respond_to?(:human_attribute_name) 136 | return object.class.human_attribute_name(attribute) 137 | end 138 | 139 | attribute.to_s.humanize 140 | end 141 | 142 | def custom_label(attribute, text, options) 143 | return block_given? ? yield.html_safe : ''.html_safe if text == false 144 | 145 | text = default_label_text(object, attribute) if text.nil? || text == true 146 | text = safe_join([yield, text.html_safe]) if block_given? 147 | label(attribute, text, options || {}) 148 | end 149 | 150 | def column_classes(options) 151 | classes = SizeClassCalculator.new(options).classes 152 | classes + ' columns' 153 | end 154 | 155 | def tag_from_options(name, options) 156 | return ''.html_safe unless options && options[:value].present? 157 | 158 | content_tag(:div, 159 | content_tag(:span, options[:value], class: name), 160 | class: column_classes(options).to_s) 161 | end 162 | 163 | def decrement_input_size(input, column, options) 164 | return unless options.present? && options.key?(column) 165 | 166 | input.send("#{column}=", 167 | (input.send(column) - options.fetch(column).to_i)) 168 | input.send('changed?=', true) 169 | end 170 | 171 | def calculate_input_size(prefix_options, postfix_options) 172 | input_size = 173 | OpenStruct.new(changed?: false, small: 12, medium: 12, large: 12) 174 | %w(small medium large).each do |size| 175 | decrement_input_size(input_size, size.to_sym, prefix_options) 176 | decrement_input_size(input_size, size.to_sym, postfix_options) 177 | end 178 | 179 | input_size 180 | end 181 | 182 | def wrap_prefix_and_postfix(block, prefix_options, postfix_options) 183 | prefix = tag_from_options('prefix', prefix_options) 184 | postfix = tag_from_options('postfix', postfix_options) 185 | 186 | input_size = calculate_input_size(prefix_options, postfix_options) 187 | klass = column_classes(input_size.marshal_dump).to_s 188 | input = content_tag(:div, block, class: klass) 189 | 190 | return block unless input_size.changed? 191 | content_tag(:div, prefix + input + postfix, class: 'row collapse') 192 | end 193 | 194 | def error_and_help_text(attribute, options = {}) 195 | html = '' 196 | if options[:help_text] 197 | html += content_tag(:p, options[:help_text], class: 'help-text') 198 | end 199 | html += error_for(attribute, options) || '' 200 | html.html_safe 201 | end 202 | 203 | def field_label(attribute, options) 204 | return ''.html_safe unless auto_labels || options[:label] 205 | custom_label(attribute, options[:label], options[:label_options]) 206 | end 207 | 208 | def field(attribute, options, html_options = nil) 209 | html = field_label(attribute, options) 210 | class_options = html_options || options 211 | 212 | if error?(attribute) 213 | class_options[:class] = class_options[:class].to_s 214 | class_options[:class] += ' is-invalid-input' 215 | end 216 | 217 | options.delete(:label) 218 | options.delete(:label_options) 219 | help_text = options.delete(:help_text) 220 | prefix = options.delete(:prefix) 221 | postfix = options.delete(:postfix) 222 | 223 | html += wrap_prefix_and_postfix(yield(class_options), prefix, postfix) 224 | html + error_and_help_text(attribute, options.merge(help_text: help_text)) 225 | end 226 | 227 | def auto_labels 228 | if @options[:auto_labels].nil? 229 | FoundationRailsHelper.configuration.auto_labels 230 | else 231 | @options[:auto_labels] 232 | end 233 | end 234 | end 235 | end 236 | -------------------------------------------------------------------------------- /lib/foundation_rails_helper/size_class_calculator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | module FoundationRailsHelper 3 | class SizeClassCalculator 4 | def initialize(size_options) 5 | @small = size_options[:small] 6 | @medium = size_options[:medium] 7 | @large = size_options[:large] 8 | end 9 | 10 | def classes 11 | [small_class, medium_class, large_class].compact.join(' ') 12 | end 13 | 14 | private 15 | 16 | def small_class 17 | "small-#{@small}" if valid_size(@small) 18 | end 19 | 20 | def medium_class 21 | "medium-#{@medium}" if valid_size(@medium) 22 | end 23 | 24 | def large_class 25 | "large-#{@large}" if valid_size(@large) 26 | end 27 | 28 | def valid_size(value) 29 | value.present? && value.to_i < 12 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /lib/foundation_rails_helper/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | module FoundationRailsHelper 3 | VERSION = '4.0.1' 4 | end 5 | -------------------------------------------------------------------------------- /lib/railtie.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | module Railtie 3 | class Railtie < Rails::Railtie 4 | # initializer 'setup foundation form builder' do 5 | # 6 | # end 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /spec/.rubocop.yml: -------------------------------------------------------------------------------- 1 | inherit_from: 2 | - ../.rubocop.yml 3 | 4 | StringLiterals: 5 | EnforcedStyle: double_quotes 6 | Exclude: 7 | - './spec_helper.rb' 8 | 9 | Metrics/BlockLength: 10 | Enabled: false 11 | -------------------------------------------------------------------------------- /spec/foundation_rails_helper/configuration_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require "spec_helper" 3 | 4 | describe FoundationRailsHelper do 5 | describe FoundationRailsHelper::Configuration do 6 | describe "#button_class" do 7 | it "default value is 'success button'" do 8 | config = FoundationRailsHelper::Configuration.new 9 | expect(config.button_class).to eq("success button") 10 | end 11 | end 12 | 13 | describe "#button_class=" do 14 | it "can set value" do 15 | config = FoundationRailsHelper::Configuration.new 16 | config.button_class = "new-class" 17 | expect(config.button_class).to eq("new-class") 18 | end 19 | end 20 | 21 | describe "#ignored_flash_keys" do 22 | it "defaults to empty" do 23 | config = FoundationRailsHelper::Configuration.new 24 | expect(config.ignored_flash_keys).to eq([]) 25 | end 26 | end 27 | 28 | describe "#ignored_flash_keys=" do 29 | it "can set the value" do 30 | config = FoundationRailsHelper::Configuration.new 31 | config.ignored_flash_keys = [:foo] 32 | expect(config.ignored_flash_keys).to eq([:foo]) 33 | end 34 | end 35 | 36 | describe "#auto_labels" do 37 | it "default value is 'true'" do 38 | config = FoundationRailsHelper::Configuration.new 39 | expect(config.auto_labels).to be(true) 40 | end 41 | end 42 | 43 | describe "#auto_labels=" do 44 | it "can set the value" do 45 | config = FoundationRailsHelper::Configuration.new 46 | config.auto_labels = false 47 | expect(config.auto_labels).to be(false) 48 | end 49 | end 50 | 51 | describe ".reset" do 52 | it "resets the configured button class" do 53 | FoundationRailsHelper.configure do |config| 54 | config.button_class = "new-class" 55 | end 56 | 57 | FoundationRailsHelper.reset 58 | 59 | config = FoundationRailsHelper.configuration 60 | expect(config.button_class).to eq("success button") 61 | end 62 | 63 | it "resets the configured ignored flash keys" do 64 | FoundationRailsHelper.configure do |config| 65 | config.ignored_flash_keys = [:new_key] 66 | end 67 | 68 | FoundationRailsHelper.reset 69 | 70 | config = FoundationRailsHelper.configuration 71 | expect(config.ignored_flash_keys).to eq([]) 72 | end 73 | 74 | it "resets the configured auto labels" do 75 | FoundationRailsHelper.configure do |config| 76 | config.auto_labels = false 77 | end 78 | 79 | FoundationRailsHelper.reset 80 | 81 | config = FoundationRailsHelper.configuration 82 | expect(config.auto_labels).to be(true) 83 | end 84 | end 85 | end 86 | end 87 | -------------------------------------------------------------------------------- /spec/foundation_rails_helper/flash_helper_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require "spec_helper" 3 | 4 | describe FoundationRailsHelper::FlashHelper do 5 | include ActionView::Context if defined?(ActionView::Context) 6 | include ActionView::Helpers::FormTagHelper 7 | include FoundationRailsHelper::FlashHelper 8 | 9 | KEY_MATCHING = FoundationRailsHelper::FlashHelper::DEFAULT_KEY_MATCHING.freeze 10 | 11 | KEY_MATCHING.each do |type, klass| 12 | it "displays flash message with #{klass} class for #{type} message" do 13 | allow(self).to receive(:flash).and_return(type.to_s => "Flash message") 14 | node = Capybara.string display_flash_messages 15 | expect(node) 16 | .to have_css("div.flash.callout.#{klass}", text: "Flash message") 17 | .and have_css("[data-close]", text: "×") 18 | end 19 | end 20 | 21 | it "handles symbol keys" do 22 | allow(self).to receive(:flash).and_return(success: "Flash message") 23 | node = Capybara.string display_flash_messages 24 | expect(node).to have_css("div.callout.success", text: "Flash message") 25 | end 26 | 27 | it "handles string keys" do 28 | allow(self).to receive(:flash).and_return("success" => "Flash message") 29 | node = Capybara.string display_flash_messages 30 | expect(node).to have_css("div.callout.success", text: "Flash message") 31 | end 32 | 33 | it "displays multiple flash messages" do 34 | allow(self).to receive(:flash) 35 | .and_return("success" => "Yay it worked", 36 | "error" => "But this other thing failed") 37 | node = Capybara.string display_flash_messages 38 | expect(node) 39 | .to have_css("div.callout.success", text: "Yay it worked") 40 | .and have_css("div.callout.alert", text: "But this other thing failed") 41 | end 42 | 43 | it "displays flash message with overridden key matching" do 44 | allow(self).to receive(:flash).and_return("notice" => "Flash message") 45 | node = 46 | Capybara.string display_flash_messages(key_matching: { notice: :alert }) 47 | expect(node).to have_css("div.callout.alert", text: "Flash message") 48 | end 49 | 50 | it "displays flash message with custom key matching" do 51 | allow(self).to receive(:flash).and_return("custom_type" => "Flash message") 52 | node = Capybara.string( 53 | display_flash_messages(key_matching: { custom_type: :custom_class }) 54 | ) 55 | expect(node).to have_css("div.callout.custom_class", text: "Flash message") 56 | end 57 | 58 | it "displays flash message with standard class if key doesn't match" do 59 | allow(self).to receive(:flash).and_return("custom_type" => "Flash message") 60 | node = Capybara.string display_flash_messages 61 | expect(node).to have_css("div.callout.primary", text: "Flash message") 62 | end 63 | 64 | context "when the flash hash contains devise internal data" do 65 | before do 66 | FoundationRailsHelper.configure do |config| 67 | config.ignored_flash_keys += [:timedout] 68 | end 69 | end 70 | 71 | it "doesn't raise an error (e.g. NoMethodError)" do 72 | allow(self).to receive(:flash).and_return("timedout" => true) 73 | expect { Capybara.string display_flash_messages }.not_to raise_error 74 | end 75 | 76 | it "doesn't display an alert for that data" do 77 | allow(self).to receive(:flash).and_return("timedout" => true) 78 | expect(display_flash_messages).to be_nil 79 | 80 | # Ideally we'd create a node using Capybara.string, as in the other 81 | # examples and set the following expectation: 82 | # expect(node).to_not have_css("div.callout") 83 | # but Capybara.string doesn't behave nicely with nil input: 84 | # the input gets assigned to the @native instance variable, 85 | # which is used by the css matcher, so we get the following error: 86 | # undefined method `css' for nil:NilClass 87 | end 88 | end 89 | 90 | context "with (closable: false) option" do 91 | it "doesn't display the close button" do 92 | allow(self).to receive(:flash).and_return(success: "Flash message") 93 | node = Capybara.string display_flash_messages(closable: false) 94 | expect(node) 95 | .to have_css("div.flash.callout.success", text: "Flash message") 96 | .and have_no_css("[data-close]", text: "×") 97 | end 98 | end 99 | end 100 | -------------------------------------------------------------------------------- /spec/foundation_rails_helper/form_builder_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require "spec_helper" 3 | 4 | describe "FoundationRailsHelper::FormHelper" do 5 | include FoundationRailsSpecHelper 6 | 7 | before do 8 | mock_everything 9 | end 10 | 11 | it "should have FoundationRailsHelper::FormHelper as default buidler" do 12 | form_for(@author) do |builder| 13 | expect(builder.class).to eq FoundationRailsHelper::FormBuilder 14 | end 15 | end 16 | 17 | it "should display labels by default" do 18 | form_for(@author) do |builder| 19 | node = Capybara.string builder.text_field(:login) 20 | expect(node).to have_css('label[for="author_login"]', text: "Login") 21 | end 22 | end 23 | 24 | it "should display labels if auto_labels option is true" do 25 | form_for(@author, auto_labels: true) do |builder| 26 | node = Capybara.string builder.text_field(:login) 27 | expect(node).to have_css('label[for="author_login"]', text: "Login") 28 | end 29 | end 30 | 31 | it "should display labels if no auto_labels option" do 32 | form_for(@author, html: { class: "myclass" }) do |builder| 33 | node = Capybara.string builder.text_field(:login) 34 | expect(node).to have_css('label[for="author_login"]', text: "Login") 35 | end 36 | end 37 | 38 | it "shouldn't display labels if auto_labels option is false" do 39 | options = { html: { class: "myclass" }, auto_labels: false } 40 | 41 | form_for(@author, options) do |builder| 42 | node = Capybara.string builder.text_field(:login) 43 | expect(node).to_not have_css('label[for="author_login"]', text: "Login") 44 | end 45 | end 46 | 47 | it "should display labels if :auto_labels is set to nil" do 48 | form_for(@author, auto_labels: nil) do |builder| 49 | node = Capybara.string builder.text_field(:login) 50 | expect(node).to have_css('label[for="author_login"]', text: "Login") 51 | end 52 | end 53 | 54 | it "should display labels if :auto_labels is set to a string" do 55 | form_for(@author, auto_labels: "false") do |builder| 56 | node = Capybara.string builder.text_field(:login) 57 | expect(node).to have_css('label[for="author_login"]', text: "Login") 58 | end 59 | end 60 | 61 | it "shouldn't display labels if :auto_labels false at configuration time" do 62 | allow(FoundationRailsHelper) 63 | .to receive_message_chain(:configuration, :auto_labels).and_return(false) 64 | 65 | form_for(@author) do |builder| 66 | node = Capybara.string builder.text_field(:login) 67 | expect(node).not_to have_css('label[for="author_login"]', text: "Login") 68 | end 69 | end 70 | 71 | describe "label" do 72 | context "when there aren't any errors and no class option is passed" do 73 | it "should not have a class attribute" do 74 | form_for(@author) do |builder| 75 | node = Capybara.string builder.text_field(:login) 76 | expect(node).to have_css('label:not([class=""])') 77 | end 78 | end 79 | end 80 | 81 | it "should not have error class multiple times" do 82 | form_for(@author) do |builder| 83 | allow(@author).to receive(:errors).and_return(login: ["required"]) 84 | node = Capybara.string builder.text_field(:login) 85 | error_class = node.find("label")["class"].split(/\s+/).keep_if do |v| 86 | v == "is-invalid-label" 87 | end 88 | expect(error_class.size).to eq 1 89 | end 90 | end 91 | end 92 | 93 | describe "prefix" do 94 | context "when input field has a prefix" do 95 | before do 96 | prefix = { small: 2, medium: 4, large: 6, value: "Prefix" } 97 | 98 | form_for(@author) do |builder| 99 | @node = Capybara.string builder.text_field(:login, prefix: prefix) 100 | end 101 | end 102 | 103 | it "wraps input in the div with class 'row collapse'" do 104 | expect(@node.find(".row.collapse")).to_not be nil 105 | end 106 | 107 | it "wraps prefix in the div with the right column size" do 108 | expect(@node.find(".row.collapse")) 109 | .to have_css("div.small-2.medium-4.large-6.columns") 110 | end 111 | 112 | it "creates prefix span with right value" do 113 | selector = "div.small-2.medium-4.large-6.columns" 114 | expect(@node.find(".row.collapse").find(selector).find("span").text) 115 | .to eq("Prefix") 116 | end 117 | 118 | it "creates prefix span with right class" do 119 | expect(@node.find(".row.collapse")).to have_css("span.prefix") 120 | end 121 | 122 | it "wraps input in the div with the right column size" do 123 | expect(@node.find(".row.collapse")) 124 | .to have_css("div.small-10.medium-8.large-6.columns") 125 | end 126 | 127 | it "has right value for the input" do 128 | selector = "div.small-10.medium-8.large-6.columns" 129 | 130 | expect(@node.find(".row.collapse").find(selector)) 131 | .to have_css('input[type="text"][name="author[login]"]') 132 | end 133 | end 134 | 135 | context "without prefix" do 136 | it "will not wrap input into a div" do 137 | form_for(@author) do |builder| 138 | node = Capybara.string builder.text_field(:login) 139 | expect(node).to_not have_css("div.row.collapse") 140 | end 141 | end 142 | end 143 | end 144 | 145 | describe "postfix" do 146 | context "when input field has a postfix" do 147 | before do 148 | postfix = { small: 2, medium: 4, large: 6, value: "Postfix" } 149 | 150 | form_for(@author) do |builder| 151 | @node = Capybara.string builder.text_field(:login, postfix: postfix) 152 | end 153 | end 154 | 155 | it "wraps input in the div with class 'row collapse'" do 156 | expect(@node.find(".row.collapse")).to_not be nil 157 | end 158 | 159 | it "wraps postfix in the div with the right column size" do 160 | expect(@node.find(".row.collapse")) 161 | .to have_css("div.small-2.medium-4.large-6.columns") 162 | end 163 | 164 | it "creates postfix span with right value" do 165 | selector = "div.small-2.medium-4.large-6.columns" 166 | 167 | expect(@node.find(".row.collapse").find(selector).find("span").text) 168 | .to eq("Postfix") 169 | end 170 | 171 | it "creates postfix span with right class" do 172 | expect(@node.find(".row.collapse")).to have_css("span.postfix") 173 | end 174 | 175 | it "wraps input in the div with the right column size" do 176 | expect(@node.find(".row.collapse")) 177 | .to have_css("div.small-10.medium-8.large-6.columns") 178 | end 179 | 180 | it "has right value for the input" do 181 | selector = "div.small-10.medium-8.large-6.columns" 182 | 183 | expect(@node.find(".row.collapse").find(selector)) 184 | .to have_css('input[type="text"][name="author[login]"]') 185 | end 186 | end 187 | 188 | context "with only one column size" do 189 | before do 190 | form_for(@author) do |builder| 191 | @small_node = Capybara.string( 192 | builder.text_field(:login, postfix: { small: 2, value: "Postfix" }) 193 | ) 194 | @medium_node = Capybara.string( 195 | builder.text_field(:login, postfix: { medium: 2, value: "Postfix" }) 196 | ) 197 | @large_node = Capybara.string( 198 | builder.text_field(:login, postfix: { large: 2, value: "Postfix" }) 199 | ) 200 | end 201 | end 202 | 203 | it "wraps postfix in the div with the right column size" do 204 | expect(@small_node.find(".row.collapse")) 205 | .to have_css("div.small-2.columns") 206 | expect(@medium_node.find(".row.collapse")) 207 | .to have_css("div.medium-2.columns") 208 | expect(@large_node.find(".row.collapse")) 209 | .to have_css("div.large-2.columns") 210 | end 211 | 212 | it "wraps input in the div with the right column size" do 213 | expect(@small_node.find(".row.collapse")) 214 | .to have_css("div.small-10.columns") 215 | expect(@medium_node.find(".row.collapse")) 216 | .to have_css("div.medium-10.columns") 217 | expect(@large_node.find(".row.collapse")) 218 | .to have_css("div.large-10.columns") 219 | end 220 | 221 | it "excludes other classes from the prefix" do 222 | expect(@small_node.find(".row.collapse")) 223 | .to_not have_css("div.medium-2.columns") 224 | expect(@small_node.find(".row.collapse")) 225 | .to_not have_css("div.large-2.columns") 226 | end 227 | 228 | it "excludes other classes from the input" do 229 | expect(@small_node.find(".row.collapse")) 230 | .to have_css("div.small-10.columns") 231 | expect(@small_node.find(".row.collapse")) 232 | .to_not have_css("div.medium-12.columns") 233 | expect(@small_node.find(".row.collapse")) 234 | .to_not have_css("div.large-12.columns") 235 | end 236 | end 237 | end 238 | 239 | describe "with both prefix and postfix" do 240 | context "when input field has a prefix" do 241 | before do 242 | prefix = { small: 2, medium: 3, large: 4, value: "Prefix" } 243 | postfix = { small: 2, medium: 3, large: 4, value: "Postfix" } 244 | 245 | form_for(@author) do |builder| 246 | @node = Capybara.string( 247 | builder.text_field(:login, prefix: prefix, postfix: postfix) 248 | ) 249 | end 250 | end 251 | 252 | it "wraps input in the div with the right column size" do 253 | expect(@node.find(".row.collapse")) 254 | .to have_css("div.small-8.medium-6.large-4.columns") 255 | end 256 | end 257 | end 258 | 259 | describe "input generators" do 260 | it "should generate text_field input" do 261 | form_for(@author) do |builder| 262 | node = Capybara.string builder.text_field(:login) 263 | expect(node).to have_css('label[for="author_login"]', text: "Login") 264 | expect(node).to have_css('input[type="text"][name="author[login]"]') 265 | expect(node.find_field("author_login").value).to eq @author.login 266 | end 267 | end 268 | 269 | it "should generate text_field input without label" do 270 | form_for(@author) do |builder| 271 | node = Capybara.string builder.text_field(:login, label: false) 272 | expect(node).to_not have_css('label[for="author_login"]', text: "Login") 273 | expect(node).to have_css('input[type="text"][name="author[login]"]') 274 | expect(node.find_field("author_login").value).to eq @author.login 275 | end 276 | end 277 | 278 | it "should generate text_field with class from options" do 279 | form_for(@author) do |builder| 280 | node = Capybara.string builder.text_field(:login, class: "righteous") 281 | expect(node) 282 | .to have_css('input.righteous[type="text"][name="author[login]"]') 283 | end 284 | end 285 | 286 | it "should generate password_field input" do 287 | form_for(@author) do |builder| 288 | node = Capybara.string builder.password_field(:password) 289 | expect(node) 290 | .to have_css('label[for="author_password"]', text: "Password") 291 | expect(node) 292 | .to have_css('input[type="password"][name="author[password]"]') 293 | expect(node.find_field("author_password").value).to be_nil 294 | end 295 | end 296 | 297 | it "should generate email_field input" do 298 | form_for(@author) do |builder| 299 | node = Capybara.string builder.email_field(:email) 300 | expect(node).to have_css('label[for="author_email"]', text: "Email") 301 | expect(node).to have_css('input[type="email"][name="author[email]"]') 302 | expect(node.find_field("author_email").value).to eq @author.email 303 | end 304 | end 305 | 306 | it "should generate url_field input" do 307 | form_for(@author) do |builder| 308 | node = Capybara.string builder.url_field(:url) 309 | expect(node).to have_css('label[for="author_url"]', text: "Url") 310 | expect(node).to have_css('input[type="url"][name="author[url]"]') 311 | expect(node.find_field("author_url").value).to eq @author.url 312 | end 313 | end 314 | 315 | it "should generate phone_field input" do 316 | form_for(@author) do |builder| 317 | node = Capybara.string builder.phone_field(:phone) 318 | expect(node).to have_css('label[for="author_phone"]', text: "Phone") 319 | expect(node).to have_css('input[type="tel"][name="author[phone]"]') 320 | expect(node.find_field("author_phone").value).to eq @author.phone 321 | end 322 | end 323 | 324 | it "should generate number_field input" do 325 | form_for(@author) do |builder| 326 | node = Capybara.string builder.number_field(:some_number) 327 | expect(node) 328 | .to have_css('label[for="author_some_number"]', text: "Some number") 329 | expect(node) 330 | .to have_css('input[type="number"][name="author[some_number]"]') 331 | expect(node.find_field("author_some_number").value) 332 | .to eq @author.some_number 333 | end 334 | end 335 | 336 | it "should generate text_area input" do 337 | form_for(@author) do |builder| 338 | node = Capybara.string builder.text_area(:description) 339 | expect(node) 340 | .to have_css('label[for="author_description"]', text: "Description") 341 | expect(node).to have_css('textarea[name="author[description]"]') 342 | expect(node.find_field("author_description").value.strip) 343 | .to eq @author.description 344 | end 345 | end 346 | 347 | it "should generate file_field input" do 348 | form_for(@author) do |builder| 349 | node = Capybara.string builder.file_field(:avatar) 350 | expect(node).to have_css('label[for="author_avatar"]', text: "Avatar") 351 | expect(node).to have_css('input[type="file"][name="author[avatar]"]') 352 | expect(node.find_field("author_avatar").value).to be_nil 353 | end 354 | end 355 | 356 | it "should generate select input" do 357 | choices = [["Choice #1", :a], ["Choice #2", :b]] 358 | 359 | form_for(@author) do |builder| 360 | node = Capybara.string builder.select(:description, choices) 361 | expect(node) 362 | .to have_css('label[for="author_description"]', text: "Description") 363 | expect(node).to have_css('select[name="author[description]"]') 364 | expect(node) 365 | .to have_css('select[name="author[description]"] option[value="a"]', 366 | text: "Choice #1") 367 | expect(node) 368 | .to have_css('select[name="author[description]"] option[value="b"]', 369 | text: "Choice #2") 370 | end 371 | end 372 | 373 | it "should generate check_box input" do 374 | label = 'label[for="author_active"]' 375 | name = '[name="author[active]"]' 376 | 377 | form_for(@author) do |builder| 378 | node = Capybara.string builder.check_box(:active) 379 | expect(node).to have_css( 380 | "#{label} input[type=\"hidden\"]#{name}[value=\"0\"]", 381 | visible: false 382 | ) 383 | expect(node) 384 | .to have_css("#{label} input[type=\"checkbox\"]#{name}") 385 | expect(node).to have_css(label, text: "Active") 386 | end 387 | end 388 | 389 | it "should generate check_box input without a label" do 390 | form_for(@author) do |builder| 391 | node = Capybara.string builder.check_box(:active, label: false) 392 | expect(node) 393 | .to have_css('input[type="hidden"][name="author[active]"][value="0"]', 394 | visible: false) 395 | expect(node) 396 | .to have_css('input[type="checkbox"][name="author[active]"]') 397 | expect(node).to_not have_css('label[for="author_active"]') 398 | end 399 | end 400 | 401 | it "should generate check_box input with a label with HTML content" do 402 | label_text = "Accepts terms and conditions" 403 | 404 | form_for(@author) do |builder| 405 | node = Capybara.string( 406 | builder.check_box(:active, label: "#{label_text}") 407 | ) 408 | 409 | expect(node) 410 | .to have_css('label[for="author_active"] a', text: label_text) 411 | end 412 | end 413 | 414 | it "should generate radio_button input" do 415 | form_for(@author) do |builder| 416 | node = Capybara.string builder.radio_button(:active, "ok") 417 | expect(node).to have_css('label[for="author_active_ok"]') 418 | expect(node).to have_css('input[type="radio"][name="author[active]"]') 419 | end 420 | end 421 | 422 | it "should generate radio_button input with a label" do 423 | form_for(@author) do |builder| 424 | node = Capybara.string( 425 | builder.radio_button(:active, true, label: "Functioning") 426 | ) 427 | expect(node) 428 | .to have_css('label[for="author_active_true"]', text: "Functioning") 429 | expect(node) 430 | .to have_css('input[type="radio"][name="author[active]"]') 431 | end 432 | end 433 | 434 | it "should generate radio_button without a label" do 435 | form_for(@author) do |builder| 436 | node = Capybara.string builder.radio_button(:active, "ok", label: false) 437 | expect(node).to_not have_css('label[for="author_active_ok"]') 438 | expect(node).to_not have_css('input[label="false"]') 439 | expect(node).to have_css('input[type="radio"][name="author[active]"]') 440 | end 441 | end 442 | 443 | it "should generate radio_button with label options" do 444 | form_for(@author) do |builder| 445 | node = Capybara.string( 446 | builder.radio_button( 447 | :active, 448 | "ok", 449 | class: "very", 450 | label_options: { class: "special" } 451 | ) 452 | ) 453 | 454 | expect(node).to have_css('label.special[for="author_active_ok"]') 455 | expect(node) 456 | .to have_css('input.very[type="radio"][name="author[active]"]') 457 | end 458 | end 459 | 460 | it "should generate date_select input" do 461 | select = "select#author_birthdate_" 462 | option = 'option[selected="selected"]' 463 | 464 | form_for(@author) do |builder| 465 | node = Capybara.string( 466 | builder.label(:birthdate) + builder.date_select(:birthdate) 467 | ) 468 | expect(node) 469 | .to have_css('label[for="author_birthdate"]', text: "Birthdate") 470 | %w(1 2 3).each do |i| 471 | expect(node).to have_css("select[name='author[birthdate(#{i}i)]']") 472 | end 473 | expect(node) 474 | .to have_css("#{select}1i #{option}[value=\"1969\"]") 475 | expect(node) 476 | .to have_css("#{select}2i #{option}[value=\"6\"]") 477 | expect(node) 478 | .to have_css("#{select}3i #{option}[value=\"18\"]") 479 | %w(4 5).each do |i| 480 | expect(node) 481 | .to_not have_css("select[name='author[birthdate(#{i}i)]']") 482 | end 483 | end 484 | end 485 | 486 | it "should generate date_select input with :discard_year => true" do 487 | select = "select#author_birthdate_" 488 | option = 'option[selected="selected"]' 489 | 490 | form_for(@author) do |builder| 491 | node = Capybara.string( 492 | builder.label(:birthdate) + 493 | builder.date_select(:birthdate, discard_year: true) 494 | ) 495 | expect(node) 496 | .to have_css('label[for="author_birthdate"]', text: "Birthdate") 497 | %w(2 3).each do |i| 498 | expect(node).to have_css("select[name='author[birthdate(#{i}i)]']") 499 | end 500 | expect(node).to_not have_css("#{select}1i #{option}[value=\"1969\"]") 501 | expect(node).to have_css("#{select}2i #{option}[value=\"6\"]") 502 | expect(node).to have_css("#{select}3i #{option}[value=\"18\"]") 503 | %w(1 4 5).each do |i| 504 | expect(node) 505 | .to_not have_css("select[name='author[birthdate(#{i}i)]']") 506 | end 507 | end 508 | end 509 | 510 | it "should generate datetime_select input" do 511 | select = "select#author_birthdate_" 512 | option = 'option[selected="selected"]' 513 | 514 | form_for(@author) do |builder| 515 | node = Capybara.string( 516 | builder.label(:birthdate) + 517 | builder.datetime_select(:birthdate) 518 | ) 519 | expect(node) 520 | .to have_css('label[for="author_birthdate"]', text: "Birthdate") 521 | %w(1 2 3 4 5).each do |i| 522 | expect(node).to have_css("select[name='author[birthdate(#{i}i)]']") 523 | end 524 | expect(node).to have_css("#{select}1i #{option}[value=\"1969\"]") 525 | expect(node).to have_css("#{select}2i #{option}[value=\"6\"]") 526 | expect(node).to have_css("#{select}3i #{option}[value=\"18\"]") 527 | expect(node).to have_css("#{select}4i #{option}[value=\"20\"]") 528 | expect(node).to have_css("#{select}5i #{option}[value=\"30\"]") 529 | end 530 | end 531 | 532 | it "should generate datetime_select input with :discard_year => true" do 533 | select = "select#author_birthdate_" 534 | option = 'option[selected="selected"]' 535 | 536 | form_for(@author) do |builder| 537 | node = Capybara.string( 538 | builder.label(:birthdate) + 539 | builder.datetime_select(:birthdate, discard_year: true) 540 | ) 541 | expect(node) 542 | .to have_css('label[for="author_birthdate"]', text: "Birthdate") 543 | %w(2 3 4 5).each do |i| 544 | expect(node).to have_css("select[name='author[birthdate(#{i}i)]']") 545 | end 546 | expect(node).to_not have_css("#{select}1i #{option}[value=\"1969\"]") 547 | expect(node).to have_css("#{select}2i #{option}[value=\"6\"]") 548 | expect(node).to have_css("#{select}3i #{option}[value=\"18\"]") 549 | expect(node).to have_css("#{select}4i #{option}[value=\"20\"]") 550 | expect(node).to have_css("#{select}5i #{option}[value=\"30\"]") 551 | expect(node).to_not have_css("select[name='author[birthdate(#1i)]']") 552 | end 553 | end 554 | 555 | it "should generate time_zone_select input" do 556 | form_for(@author) do |builder| 557 | node = Capybara.string( 558 | builder.label(:time_zone) + builder.time_zone_select(:time_zone) 559 | ) 560 | expect(node) 561 | .to have_css('label[for="author_time_zone"]', text: "Time zone") 562 | expect(node).to have_css('select[name="author[time_zone]"]') 563 | expect(node) 564 | .to have_css('select[name="author[time_zone]"] option[value="Perth"]', 565 | text: "(GMT+08:00) Perth") 566 | end 567 | end 568 | 569 | it "should generate date_field input" do 570 | form_for(@author) do |builder| 571 | node = Capybara.string builder.date_field(:publish_date) 572 | expect(node) 573 | .to have_css('label[for="author_publish_date"]', text: "date") 574 | expect(node) 575 | .to have_css('input[type="date"][name="author[publish_date]"]') 576 | expect(node.find_field("author_publish_date").value) 577 | .to eq @author.publish_date.to_s 578 | end 579 | end 580 | 581 | it "should generate datetime_field input" do 582 | form_for(@author) do |builder| 583 | node = Capybara.string builder.datetime_field(:forty_two) 584 | expect(node) 585 | .to have_css('label[for="author_forty_two"]', text: "Forty two") 586 | expect(node) 587 | .to have_css('input[type^="datetime"][name="author[forty_two]"]') 588 | value = DateTime.parse(node.find_field("author_forty_two").value) 589 | expect(value).to eq @author.forty_two.to_s 590 | end 591 | end 592 | 593 | it "should generate datetime_local_field" do 594 | form_for(@author) do |builder| 595 | node = Capybara.string builder.datetime_local_field(:forty_two) 596 | expect(node) 597 | .to have_css('label[for="author_forty_two"]', text: "Forty two") 598 | expect(node) 599 | .to have_css('input[type="datetime-local"][name="author[forty_two]"]') 600 | expect(node.find_field("author_forty_two").value) 601 | .to eq @author.forty_two.strftime("%Y-%m-%dT%H:%M:%S") 602 | end 603 | end 604 | 605 | it "should generate month_field input" do 606 | form_for(@author) do |builder| 607 | node = Capybara.string builder.month_field(:forty_two) 608 | expect(node) 609 | .to have_css('label[for="author_forty_two"]', text: "Forty two") 610 | expect(node) 611 | .to have_css('input[type="month"][name="author[forty_two]"]') 612 | expect(node.find_field("author_forty_two").value) 613 | .to eq @author.forty_two.strftime("%Y-%m") 614 | end 615 | end 616 | 617 | it "should generate week_field" do 618 | form_for(@author) do |builder| 619 | node = Capybara.string builder.week_field(:forty_two) 620 | expect(node) 621 | .to have_css('label[for="author_forty_two"]', text: "Forty two") 622 | expect(node) 623 | .to have_css('input[type="week"][name="author[forty_two]"]') 624 | expect(node.find_field("author_forty_two").value) 625 | .to eq @author.forty_two.strftime("%Y-W%V") 626 | end 627 | end 628 | 629 | it "should generate time_field" do 630 | form_for(@author) do |builder| 631 | node = Capybara.string builder.time_field(:forty_two) 632 | expect(node) 633 | .to have_css('label[for="author_forty_two"]', text: "Forty two") 634 | expect(node) 635 | .to have_css('input[type="time"][name="author[forty_two]"]') 636 | expect(node.find_field("author_forty_two").value) 637 | .to eq @author.forty_two.strftime("%H:%M:%S.%L") 638 | end 639 | end 640 | 641 | it "should generate range_field" do 642 | form_for(@author) do |builder| 643 | node = Capybara.string builder.range_field(:some_number) 644 | expect(node) 645 | .to have_css('label[for="author_some_number"]', text: "Some number") 646 | expect(node) 647 | .to have_css('input[type="range"][name="author[some_number]"]') 648 | expect(node.find_field("author_some_number").value) 649 | .to eq @author.some_number 650 | end 651 | end 652 | 653 | it "should generate search_field" do 654 | form_for(@author) do |builder| 655 | node = Capybara.string builder.search_field(:description) 656 | expect(node) 657 | .to have_css('label[for="author_description"]', text: "Description") 658 | expect(node) 659 | .to have_css('input[type="search"][name="author[description]"]') 660 | expect(node.find_field("author_description").value) 661 | .to eq @author.description 662 | end 663 | end 664 | 665 | it "should generate color_field" do 666 | form_for(@author) do |builder| 667 | node = Capybara.string builder.color_field(:favorite_color) 668 | expect(node).to have_css( 669 | 'label[for="author_favorite_color"]', text: "Favorite color" 670 | ) 671 | expect(node) 672 | .to have_css('input[type="color"][name="author[favorite_color]"]') 673 | expect(node.find_field("author_favorite_color").value) 674 | .to eq @author.favorite_color 675 | end 676 | end 677 | 678 | it "should generate collection_select input" do 679 | selector = 'select[name="author[favorite_book]"] option' 680 | 681 | form_for(@author) do |builder| 682 | node = Capybara.string( 683 | builder.collection_select(:favorite_book, Book.all, :id, :title) 684 | ) 685 | expect(node).to have_css( 686 | 'label[for="author_favorite_book"]', 687 | text: "Favorite book" 688 | ) 689 | expect(node).to have_css('select[name="author[favorite_book]"]') 690 | expect(node) 691 | .to have_css("#{selector}[value=\"78\"]", text: "Gulliver's Travels") 692 | expect(node) 693 | .to have_css("#{selector}[value=\"133\"]", text: "Treasure Island") 694 | end 695 | end 696 | 697 | it "should generate grouped_collection_select input" do 698 | selector = 'select[name="author[favorite_book]"] optgroup' 699 | 700 | form_for(@author) do |builder| 701 | node = Capybara.string( 702 | builder.grouped_collection_select(:favorite_book, Genre.all, 703 | :books, :name, :id, :title) 704 | ) 705 | expect(node).to have_css( 706 | 'label[for="author_favorite_book"]', text: "Favorite book" 707 | ) 708 | expect(node).to have_css('select[name="author[favorite_book]"]') 709 | expect(node).to have_css( 710 | "#{selector}[label=\"Exploration\"] option[value=\"78\"]", 711 | text: "Gulliver's Travels" 712 | ) 713 | expect(node).to have_css( 714 | "#{selector}[label=\"Pirate Exploits\"] option[value=\"133\"]", 715 | text: "Treasure Island" 716 | ) 717 | end 718 | end 719 | 720 | describe "help_text" do 721 | it "should add a p element" do 722 | form_for(@author) do |builder| 723 | help_text = "Enter login" 724 | node = 725 | Capybara.string builder.text_field(:login, help_text: help_text) 726 | expect(node.find("p").text).to eq help_text 727 | end 728 | end 729 | 730 | it "should not add help_text attribute" do 731 | form_for(@author) do |builder| 732 | node = 733 | Capybara.string builder.text_field(:login, help_text: "Enter login") 734 | expect(node.find_field("author_login")["help_text"]).to be_nil 735 | end 736 | end 737 | end 738 | 739 | context "when there aren't any errors and no class option is passed" do 740 | it "should not have a class attribute" do 741 | form_for(@author) do |builder| 742 | node = Capybara.string builder.text_field(:login) 743 | expect(node).to have_css('input:not([class=""])') 744 | end 745 | end 746 | end 747 | end 748 | 749 | describe "errors generator" do 750 | it "should not display errors" do 751 | form_for(@author) do |builder| 752 | node = Capybara.string builder.text_field(:login) 753 | expect(node).to_not have_css("small.form-error") 754 | end 755 | end 756 | 757 | it "should display errors" do 758 | form_for(@author) do |builder| 759 | allow(@author).to receive(:errors).and_return(login: ["required"]) 760 | node = Capybara.string builder.text_field(:login) 761 | expect(node).to have_css("small.form-error", text: "required") 762 | end 763 | end 764 | 765 | %w(file_field email_field text_field telephone_field phone_field 766 | url_field number_field range_field search_field color_field 767 | password_field).each do |field| 768 | it "should display errors on #{field} inputs" do 769 | form_for(@author) do |builder| 770 | allow(@author) 771 | .to receive(:errors).and_return(description: ["required"]) 772 | node = Capybara.string builder.public_send(field, :description) 773 | expect(node) 774 | .to have_css('label.is-invalid-label[for="author_description"]') 775 | expect(node) 776 | .to have_css('input.is-invalid-input[name="author[description]"]') 777 | end 778 | end 779 | end 780 | 781 | %w(date_field datetime_field datetime_local_field month_field 782 | week_field time_field).each do |field| 783 | it "should display errors on #{field} inputs" do 784 | form_for(@author) do |builder| 785 | allow(@author) 786 | .to receive(:errors).and_return(birthdate: ["required"]) 787 | node = Capybara.string builder.public_send(field, :birthdate) 788 | expect(node) 789 | .to have_css('label.is-invalid-label[for="author_birthdate"]') 790 | expect(node) 791 | .to have_css('input.is-invalid-input[name="author[birthdate]"]') 792 | end 793 | end 794 | end 795 | 796 | it "should display errors on text_area inputs" do 797 | form_for(@author) do |builder| 798 | allow(@author).to receive(:errors).and_return(description: ["required"]) 799 | node = Capybara.string builder.text_area(:description) 800 | expect(node) 801 | .to have_css('label.is-invalid-label[for="author_description"]') 802 | expect(node) 803 | .to have_css('textarea.is-invalid-input[name="author[description]"]') 804 | end 805 | end 806 | 807 | it "should display errors on select inputs" do 808 | form_for(@author) do |builder| 809 | allow(@author) 810 | .to receive(:errors).and_return(favorite_book: ["required"]) 811 | node = Capybara.string( 812 | builder.select(:favorite_book, [["Choice #1", :a], ["Choice #2", :b]]) 813 | ) 814 | expect(node) 815 | .to have_css('label.is-invalid-label[for="author_favorite_book"]') 816 | expect(node) 817 | .to have_css('select.is-invalid-input[name="author[favorite_book]"]') 818 | end 819 | end 820 | 821 | it "should display errors on date_select inputs" do 822 | form_for(@author) do |builder| 823 | allow(@author).to receive(:errors).and_return(birthdate: ["required"]) 824 | node = Capybara.string builder.date_select(:birthdate) 825 | expect(node) 826 | .to have_css('label.is-invalid-label[for="author_birthdate"]') 827 | %w(1 2 3).each do |i| 828 | expect(node).to have_css( 829 | "select.is-invalid-input[name='author[birthdate(#{i}i)]']" 830 | ) 831 | end 832 | end 833 | end 834 | 835 | it "should display errors on datetime_select inputs" do 836 | form_for(@author) do |builder| 837 | allow(@author).to receive(:errors).and_return(birthdate: ["required"]) 838 | node = Capybara.string builder.datetime_select(:birthdate) 839 | expect(node) 840 | .to have_css('label.is-invalid-label[for="author_birthdate"]') 841 | %w(1 2 3 4 5).each do |i| 842 | expect(node).to have_css( 843 | "select.is-invalid-input[name='author[birthdate(#{i}i)]']" 844 | ) 845 | end 846 | end 847 | end 848 | 849 | it "should display errors on time_zone_select inputs" do 850 | form_for(@author) do |builder| 851 | allow(@author).to receive(:errors).and_return(time_zone: ["required"]) 852 | node = Capybara.string builder.time_zone_select(:time_zone) 853 | expect(node) 854 | .to have_css('label.is-invalid-label[for="author_time_zone"]') 855 | expect(node) 856 | .to have_css('select.is-invalid-input[name="author[time_zone]"]') 857 | end 858 | end 859 | 860 | it "should display errors on collection_select inputs" do 861 | form_for(@author) do |builder| 862 | allow(@author) 863 | .to receive(:errors).and_return(favorite_book: ["required"]) 864 | node = Capybara.string( 865 | builder.collection_select(:favorite_book, Book.all, :id, :title) 866 | ) 867 | expect(node) 868 | .to have_css('label.is-invalid-label[for="author_favorite_book"]') 869 | expect(node) 870 | .to have_css('select.is-invalid-input[name="author[favorite_book]"]') 871 | end 872 | end 873 | 874 | it "should display errors on grouped_collection_select inputs" do 875 | form_for(@author) do |builder| 876 | allow(@author) 877 | .to receive(:errors).and_return(favorite_book: ["required"]) 878 | node = Capybara.string( 879 | builder.grouped_collection_select( 880 | :favorite_book, 881 | Genre.all, 882 | :books, 883 | :name, 884 | :id, 885 | :title 886 | ) 887 | ) 888 | expect(node) 889 | .to have_css('label.is-invalid-label[for="author_favorite_book"]') 890 | expect(node) 891 | .to have_css('select.is-invalid-input[name="author[favorite_book]"]') 892 | end 893 | end 894 | 895 | # N.B. check_box and radio_button inputs don't have the is-invalid-input 896 | # class applied 897 | it "should display HTML errors when the option is specified" do 898 | login = ['required link'] 899 | 900 | form_for(@author) do |builder| 901 | allow(@author).to receive(:errors).and_return(login: login) 902 | node = Capybara.string( 903 | builder.text_field(:login, html_safe_errors: true) 904 | ) 905 | expect(node).to have_link("link", href: "link_target") 906 | end 907 | end 908 | 909 | it "should not display HTML errors when the option is not specified" do 910 | login = ['required link'] 911 | 912 | form_for(@author) do |builder| 913 | allow(@author).to receive(:errors).and_return(login: login) 914 | node = Capybara.string builder.text_field(:login) 915 | expect(node).to_not have_link("link", href: "link") 916 | end 917 | end 918 | 919 | it "should not display labels unless specified in the builder method" do 920 | label = "Tell me about you" 921 | 922 | form_for(@author, auto_labels: false) do |builder| 923 | node = Capybara.string( 924 | builder.text_field(:login) + 925 | builder.check_box(:active, label: true) + 926 | builder.text_field(:description, label: label) 927 | ) 928 | 929 | expect(node).to_not have_css('label[for="author_login"]') 930 | expect(node).to have_css('label[for="author_active"]', text: "Active") 931 | expect(node) 932 | .to have_css('label[for="author_description"]', text: label) 933 | end 934 | end 935 | 936 | context "when class option given" do 937 | it "should add it to the error class" do 938 | form_for(@author) do |builder| 939 | allow(@author).to receive(:errors).and_return(email: ["required"]) 940 | node = Capybara.string builder.text_field(:email, class: "righteous") 941 | expect(node).to have_css( 942 | 'input.righteous.is-invalid-input[name="author[email]"]' 943 | ) 944 | end 945 | end 946 | end 947 | 948 | context "when invalid class option given" do 949 | it "should add it to the error class" do 950 | form_for(@author) do |builder| 951 | allow(@author).to receive(:errors).and_return(email: ["required"]) 952 | node = Capybara.string builder.text_field(:email, class: :illgotten) 953 | expect(node).to have_css( 954 | 'input.illgotten.is-invalid-input[name="author[email]"]' 955 | ) 956 | end 957 | end 958 | end 959 | end 960 | 961 | describe "submit button generator" do 962 | after :each do 963 | FoundationRailsHelper.reset 964 | end 965 | 966 | context "when button_class config is not set" do 967 | it "should display form button with default class" do 968 | form_for(@author) do |builder| 969 | node = Capybara.string builder.submit("Save") 970 | expect(node) 971 | .to have_css('input[type="submit"][class="success button"]') 972 | end 973 | end 974 | end 975 | 976 | context 'when button_class config is "superduper"' do 977 | before do 978 | FoundationRailsHelper.configure do |config| 979 | config.button_class = "superduper" 980 | end 981 | end 982 | 983 | it "should display form button with 'superduper' class" do 984 | form_for(@author) do |builder| 985 | node = Capybara.string builder.submit("Save") 986 | expect(node).to have_css('input[type="submit"][class="superduper"]') 987 | end 988 | end 989 | end 990 | 991 | context 'when option value is "superduper"' do 992 | it "should display form button with 'superduper' class" do 993 | form_for(@author) do |builder| 994 | node = Capybara.string builder.submit("Save", class: "superduper") 995 | expect(node).to have_css('input[type="submit"][class="superduper"]') 996 | end 997 | end 998 | end 999 | end 1000 | end 1001 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each { |f| require f } 3 | require "foundation_rails_helper" 4 | require "capybara" 5 | 6 | # turning off deprecations 7 | ActiveSupport::Deprecation.silenced = true 8 | I18n.enforce_available_locales = false 9 | -------------------------------------------------------------------------------- /spec/support/.rubocop.yml: -------------------------------------------------------------------------------- 1 | inherit_from: 2 | - ../../.rubocop.yml 3 | -------------------------------------------------------------------------------- /spec/support/classes/author.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require 'active_model' 3 | 4 | class Author 5 | extend ActiveModel::Naming if defined?(ActiveModel::Naming) 6 | include ActiveModel::Conversion if defined?(ActiveModel::Conversion) 7 | 8 | def to_label 9 | end 10 | 11 | def persisted? 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /spec/support/classes/book.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require 'active_model' 3 | 4 | class Book 5 | extend ActiveModel::Naming if defined?(ActiveModel::Naming) 6 | include ActiveModel::Conversion if defined?(ActiveModel::Conversion) 7 | 8 | def to_label 9 | end 10 | 11 | def persisted? 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /spec/support/classes/genre.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require 'active_model' 3 | 4 | class Genre 5 | extend ActiveModel::Naming if defined?(ActiveModel::Naming) 6 | include ActiveModel::Conversion if defined?(ActiveModel::Conversion) 7 | 8 | def to_label 9 | end 10 | 11 | def persisted? 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /spec/support/mock_rails.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require 'bundler/setup' 3 | require 'action_view' 4 | require 'action_controller' 5 | require 'active_model' 6 | require 'active_support/core_ext' 7 | 8 | # Thanks to Justin French for formtastic spec 9 | module FoundationRailsSpecHelper 10 | include ActionView::Helpers::FormHelper 11 | include ActionView::Helpers::FormOptionsHelper 12 | include ActionView::Helpers::DateHelper 13 | if defined?(ActionController::PolymorphicRoutes) 14 | include ActionController::PolymorphicRoutes 15 | end 16 | include ActionDispatch::Routing::PolymorphicRoutes 17 | include AbstractController::UrlFor if defined?(AbstractController::UrlFor) 18 | # to use dom_class in Rails 4 tests 19 | # in Rails 5, RecordIdentifier is already required by FormHelper module 20 | include ActionView::RecordIdentifier 21 | 22 | def active_model_validator(kind, attributes, options = {}) 23 | validator = mock( 24 | "ActiveModel::Validations::#{kind.to_s.titlecase}Validator", 25 | attributes: attributes, 26 | options: options 27 | ) 28 | allow(validator).to receive(:kind).and_return(kind) 29 | validator 30 | end 31 | 32 | def active_model_presence_validator(attributes, options = {}) 33 | active_model_validator(:presence, attributes, options) 34 | end 35 | 36 | def active_model_length_validator(attributes, options = {}) 37 | active_model_validator(:length, attributes, options) 38 | end 39 | 40 | def active_model_inclusion_validator(attributes, options = {}) 41 | active_model_validator(:inclusion, attributes, options) 42 | end 43 | 44 | def active_model_numericality_validator(attributes, options = {}) 45 | active_model_validator(:numericality, attributes, options) 46 | end 47 | 48 | def mock_everything 49 | mock_author 50 | mock_books 51 | mock_genres 52 | end 53 | 54 | # Resource-oriented styles like form_for(@post) will expect a path method 55 | # for the object, so we're defining some here. 56 | # the argument is required for Rails 4 tests 57 | def authors_path(*_args) 58 | '/authors' 59 | end 60 | 61 | def _routes 62 | double('_routes', polymorphic_mappings: {}) 63 | end 64 | 65 | def self.included(base) 66 | base.class_eval do 67 | attr_accessor :output_buffer 68 | 69 | def protect_against_forgery? 70 | false 71 | end 72 | end 73 | end 74 | 75 | private 76 | 77 | def mock_author 78 | @author = ::Author.new 79 | { login: 'fred_smith', email: 'fred@foo.com', url: 'http://example.com', 80 | some_number: '42', phone: '317 456 2564', active: true, 81 | description: 'bla bla bla', birthdate: DateTime.parse('1969-06-18 20:30'), 82 | forty_two: DateTime.parse('1969-06-18 20:30') + 42.years, 83 | time_zone: 'Perth', publish_date: Date.new(2000, 1, 1), 84 | favorite_color: '#424242', favorite_book: nil }.each do |m, v| 85 | allow(@author).to receive(m).and_return(v) 86 | end 87 | end 88 | 89 | def mock_books 90 | mock_book_0 91 | mock_book_1 92 | allow(::Book).to receive(:all).and_return([@book_0, @book_1]) 93 | end 94 | 95 | def mock_book_0 96 | @book_0 = ::Book.new 97 | allow(@book_0).to receive(:id).and_return('78') 98 | allow(@book_0).to receive(:title).and_return("Gulliver's Travels") 99 | end 100 | 101 | def mock_book_1 102 | @book_1 = ::Book.new 103 | allow(@book_1).to receive(:id).and_return('133') 104 | allow(@book_1).to receive(:title).and_return('Treasure Island') 105 | end 106 | 107 | def mock_genres 108 | mock_genre_0 109 | mock_genre_1 110 | allow(::Genre).to receive(:all).and_return([@genre_0, @genre_1]) 111 | end 112 | 113 | def mock_genre_0 114 | @genre_0 = ::Genre.new 115 | allow(@genre_0).to receive(:name).and_return('Exploration') 116 | allow(@genre_0).to receive(:books).and_return([@book_0]) 117 | end 118 | 119 | def mock_genre_1 120 | @genre_1 = ::Genre.new 121 | allow(@genre_1).to receive(:name).and_return('Pirate Exploits') 122 | allow(@genre_1).to receive(:books).and_return([@book_1]) 123 | end 124 | end 125 | --------------------------------------------------------------------------------