diff --git a/Gemfile b/Gemfile index 4b3b92e4..38d710f1 100644 --- a/Gemfile +++ b/Gemfile @@ -27,12 +27,14 @@ when /\// # A path gem 'activerecord', path: "#{rails}/activerecord", require: false gem 'actionpack', path: "#{rails}/actionpack" gem 'actionview', path: "#{rails}/actionview" + gem 'railties', path: "#{rails}/railties" when /^v/ # A tagged version git 'https://github.com/rails/rails.git', tag: rails do gem 'activesupport' gem 'activemodel' gem 'activerecord', require: false gem 'actionpack' + gem 'railties' end else git 'https://github.com/rails/rails.git', branch: rails do @@ -40,6 +42,7 @@ else gem 'activemodel' gem 'activerecord', require: false gem 'actionpack' + gem 'railties' end end gem 'mysql2' diff --git a/docs/docs/going-further/other-notes.md b/docs/docs/going-further/other-notes.md index 68785bcb..2c2ceac9 100644 --- a/docs/docs/going-further/other-notes.md +++ b/docs/docs/going-further/other-notes.md @@ -414,15 +414,33 @@ artists.result.to_sql OR \"musicians\".\"email\" ILIKE '%bar%'))" ``` -### Using SimpleForm +### Using SimpleForm with Ransack -If you would like to combine the Ransack and SimpleForm form builders, set the -`RANSACK_FORM_BUILDER` environment variable before Rails boots up, e.g. in -`config/application.rb` before `require 'rails/all'` as shown below (and add -`gem 'simple_form'` in your Gemfile). +To integrate Ransack with SimpleForm, use the `search_simple_form_for` helper +instead of `search_form_for`. This helper specifically utilizes SimpleForm's +capabilities to construct your form, ensuring that form fields are +automatically tailored to match the expected input types based on the +attribute definitions. -```ruby -require File.expand_path('../boot', __FILE__) -ENV['RANSACK_FORM_BUILDER'] = '::SimpleForm::FormBuilder' -require 'rails/all' +```erb +<%= search_simple_form_for @q do |f| %> + <%= f.input :name_cont %> + <%= f.input :employee_name_present %> + <%= f.input :created_at_gteq %> + <%= f.button :submit %> +<% end %> ``` + +In the example above, if you are employing Bootstrap wrappers with SimpleForm, +the generated form will include appropriate wrapper **divs** and **classes**. +The form builder intelligently determines the input type: it uses **boolean** +for predicates ending in **_present** and **date** for attributes like +**created_at**. + +**Important Update:** Previously, integrating SimpleForm with Ransack required +setting the environment variable `RANSACK_FORM_BUILDER` to +`::SimpleForm::FormBuilder`. + +With the introduction of the `search_simple_form_for` helper, this workaround +is obsolete and will be deprecated. Using the new helper is the recommended +approach moving forward. diff --git a/lib/ransack/helpers.rb b/lib/ransack/helpers.rb index 799e53e7..917ad23f 100644 --- a/lib/ransack/helpers.rb +++ b/lib/ransack/helpers.rb @@ -1,2 +1,3 @@ require 'ransack/helpers/form_builder' +require 'ransack/helpers/simple_form_builder' if defined?(::SimpleForm) require 'ransack/helpers/form_helper' diff --git a/lib/ransack/helpers/form_builder.rb b/lib/ransack/helpers/form_builder.rb index 0f9d826f..3916b3ec 100644 --- a/lib/ransack/helpers/form_builder.rb +++ b/lib/ransack/helpers/form_builder.rb @@ -28,13 +28,16 @@ module Helpers class FormBuilder < (ENV[RANSACK_FORM_BUILDER].try(:constantize) || ActionView::Helpers::FormBuilder) - def label(method, *args, &block) - options = args.extract_options! - text = args.first + def label_text(method, text=nil, options={}) i18n = options[:i18n] || {} text ||= object.translate( method, i18n.reverse_merge(include_associations: true) ) if object.respond_to? :translate + end + + def label(method, *args, &block) + options = args.extract_options! + text = label_text(method, args.first, options) super(method, text, options, &block) end diff --git a/lib/ransack/helpers/form_helper.rb b/lib/ransack/helpers/form_helper.rb index 77e11784..2b074c7e 100644 --- a/lib/ransack/helpers/form_helper.rb +++ b/lib/ransack/helpers/form_helper.rb @@ -34,6 +34,19 @@ def search_form_for(record, options = {}, &proc) form_for(record, options, &proc) end + # +search_simple_form_for+ + # + # <%= search_simple_form_for(@q) do |f| %> + # + # This is a shortcut to: + # + # <%= search_form_for(@q, builder: Ransack::Helpers::SimpleFormBuilder) do |f| %> + # + + def search_simple_form_for(record, options = {}, &proc) + search_form_for(record, options.merge(builder: Ransack::Helpers::SimpleFormBuilder), &proc) + end + # +sort_link+ # # <%= sort_link(@q, :name, [:name, 'kind ASC'], 'Player Name') %> diff --git a/lib/ransack/helpers/simple_form_builder.rb b/lib/ransack/helpers/simple_form_builder.rb new file mode 100644 index 00000000..e207f894 --- /dev/null +++ b/lib/ransack/helpers/simple_form_builder.rb @@ -0,0 +1,18 @@ +module Ransack + module Helpers + class SimpleFormBuilder < ::Ransack::Helpers::FormBuilder + attr_reader :simple_form_builder + + def initialize(*args, **kwargs) + @simple_form_builder = ::SimpleForm::FormBuilder.new(*args, **kwargs) + super + end + + def input(attribute_name, options = {}, &block) + options[:label] ||= label_text(attribute_name, nil, options) + options[:required] ||= false + simple_form_builder.input(attribute_name, options, &block) + end + end + end +end diff --git a/lib/ransack/search.rb b/lib/ransack/search.rb index a8bf95f8..4e721d19 100644 --- a/lib/ransack/search.rb +++ b/lib/ransack/search.rb @@ -128,6 +128,41 @@ def inspect "Ransack::Search<#{details}>" end + # Determines if the given attribute exists within the base object (Nodes::Grouping). + # This method is particularly useful for form builders like SimpleForm, + # which rely on such checks to build appropriate input fields dynamically. + # + # @param attr_name [String, Symbol] the name of the attribute to check + # @return [Boolean] truethy if the attribute exists, falsey otherwise + def has_attribute?(attr_name) + base.attribute_method?(attr_name.to_s) + end + + # Retrieves the type of a specified attribute, used primarily by form builders + # like SimpleForm to correctly generate input fields. It leverages private + # method calls within the Nodes::Condition to deduce attribute details + # such as name, predicate, and combinator. + # + # If the predicate defines a type, the predicate is returned. + # Otherwise, this method fetches the type based on the search context's object, + # defaulting to the type of the first attribute if multiple are specified. + # The form builder will call #type on the returned value to get a Symbol + # representing the type of input field to generate (e.g. :boolean, :string, :date). + # + # @param attr_name [String, Symbol] the name of the attribute for which the type is requested + # @return [Object] the predicate or AR type (both should respond to #type). + + def type_for_attribute(attr_name) + attributes, predicate, _combinator = + Nodes::Condition.send( + :extract_values_for_condition, + attr_name, + context + ) + + predicate.type ? predicate : context.object.type_for_attribute(attributes.first) + end + private def add_scope(key, args) diff --git a/ransack.gemspec b/ransack.gemspec index 3e4e15fd..f23f70ab 100644 --- a/ransack.gemspec +++ b/ransack.gemspec @@ -19,6 +19,8 @@ Gem::Specification.new do |s| s.add_dependency 'activesupport', '>= 6.1.5' s.add_dependency 'i18n' + s.add_development_dependency 'simple_form' + s.files = `git ls-files`.split("\n") s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) } diff --git a/spec/ransack/helpers/form_helper_spec.rb b/spec/ransack/helpers/form_helper_spec.rb index a77ca4f0..43aee9e9 100644 --- a/spec/ransack/helpers/form_helper_spec.rb +++ b/spec/ransack/helpers/form_helper_spec.rb @@ -850,12 +850,39 @@ module Helpers before do Ransack.configure { |c| c.search_key = :example } end + + after do + Ransack.configure { |c| c.search_key = nil } + end + subject { @controller.view_context .search_form_for(Person.ransack) { |f| f.text_field :name_eq } } it { should match /example_name_eq/ } end + + describe "#search_form_for default builder" do + subject { + @controller.view_context + .search_form_for(Person.ransack) { |f| return f.class } + } + it { should be Ransack::Helpers::FormBuilder } + end + + describe "#search_simple_form_for default builder" do + subject { + @controller.view_context + .search_simple_form_for(Person.ransack) { |f| return f.class } + } + it { should be Ransack::Helpers::SimpleFormBuilder } + end + + describe '#search_simple_form_for with default format' do + subject { @controller.view_context + .search_simple_form_for(Person.ransack) {} } + it { should match /action="\/people"/ } + end end end end diff --git a/spec/ransack/helpers/simple_form_builder_spec.rb b/spec/ransack/helpers/simple_form_builder_spec.rb new file mode 100644 index 00000000..df009516 --- /dev/null +++ b/spec/ransack/helpers/simple_form_builder_spec.rb @@ -0,0 +1,83 @@ +require 'spec_helper' + +module Ransack + module Helpers + describe SimpleFormBuilder do + + router = ActionDispatch::Routing::RouteSet.new + router.draw do + resources :people, :comments, :notes + end + + include router.url_helpers + + # FIXME: figure out a cleaner way to get this behavior + before do + @controller = ActionView::TestCase::TestController.new + @controller.instance_variable_set(:@_routes, router) + @controller.class_eval { include router.url_helpers } + @controller.view_context_class.class_eval { include router.url_helpers } + @s = Person.ransack + @controller.view_context.search_simple_form_for(@s) { |f| @f = f } + end + + describe "#input (from SimpleForm)" do + context "with :name_cont predicate" do + subject { @f.input(:name_cont) } + + it "should generate a wrapping div with both label and input inside" do + expect(subject).to match(/.*?.*?<\/div>/) + end + + it "the wrapping div should have class 'q_name_cont'" do + expect(subject).to match(//) + end + + it "should generate correct label text with predicate from locale files" do + expect(subject).to match(/.*?Full Name contains.*?<\/label>/) + end + + it "should generate correct input name=\"q[name_cont]\"" do + expect(subject).to match(//) + end + + it "should generate correct input id=\"q_name_cont\"" do + expect(subject).to match(//) + end + + it "should generate correct input type=\"text\"" do + expect(subject).to match(//) + end + end + + context "may be able to guess the type of the attribute" do + context "with :name_present (boolean) predicate" do + subject { @f.input(:name_present) } + + it "the wrapping div should have class \"boolean\"" do + expect(subject).to match(//) + end + + it "should generate correct input type=\"checkbox\"" do + expect(subject).to match(//) + end + end + + context "with :life_start_gteq predicate / date attribute " do + subject { @f.input(:life_start_gteq) } + + it "the wrapping div should have class 'input date'" do + expect(subject).to match(//) + end + + it "should generate selects for the fields of the date (year, month, day), at least" do + expect(subject).to match(//) + expect(subject).to match(//) + expect(subject).to match(//) + end + end + end + end + end + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 0c4bf109..cb8fb738 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -4,6 +4,7 @@ require 'faker' require 'ransack' require 'action_controller' +require 'simple_form' require 'ransack/helpers' require 'pry' require 'simplecov'