From a1c670d0ad511b103ba3d40546af216f109666ea Mon Sep 17 00:00:00 2001 From: "Abinoam P. Marques Jr" Date: Sun, 7 Apr 2024 21:17:24 -0300 Subject: [PATCH 01/12] Add first spec for #search_simple_form_for --- spec/ransack/helpers/form_helper_spec.rb | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/spec/ransack/helpers/form_helper_spec.rb b/spec/ransack/helpers/form_helper_spec.rb index a77ca4f0..e3c1655f 100644 --- a/spec/ransack/helpers/form_helper_spec.rb +++ b/spec/ransack/helpers/form_helper_spec.rb @@ -856,6 +856,12 @@ module Helpers } it { should match /example_name_eq/ } 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 From 24a471777248e207107ec02a2bd2e7cb028d077a Mon Sep 17 00:00:00 2001 From: "Abinoam P. Marques Jr" Date: Mon, 8 Apr 2024 10:57:51 -0300 Subject: [PATCH 02/12] Add #search_simple_form_for with custom builder Next: - Implement the builder --- lib/ransack/helpers/form_helper.rb | 13 +++++++++++++ 1 file changed, 13 insertions(+) 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') %> From c0c97218eeb67c12653b998bca44c2311ef96923 Mon Sep 17 00:00:00 2001 From: "Abinoam P. Marques Jr" Date: Mon, 8 Apr 2024 10:59:54 -0300 Subject: [PATCH 03/12] Add Ransack::Helpers::SimpleFormBuilder * It inherits from normal Ransack::Helpers::FormBuilder * It composes with an instance of SimpleForm:Builder, so it may delegate methods like SimpleForm "input" Next: - Extract "label_text" - Resolve simple_form dependency --- lib/ransack/helpers.rb | 1 + lib/ransack/helpers/simple_form_builder.rb | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+) create mode 100644 lib/ransack/helpers/simple_form_builder.rb 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/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 From b929d5bda4f1cbbc940aa8426aba1f4a24f7da32 Mon Sep 17 00:00:00 2001 From: "Abinoam P. Marques Jr" Date: Mon, 8 Apr 2024 11:05:33 -0300 Subject: [PATCH 04/12] Extracts #label_text to inject as SimpleForm label This allows SimpleForm to use the correct label like "name_cont" -> "Name contains" --- lib/ransack/helpers/form_builder.rb | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) 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 From c2ac845a8d298210e5c2bb78b277e2756b5d15e8 Mon Sep 17 00:00:00 2001 From: "Abinoam P. Marques Jr" Date: Mon, 8 Apr 2024 11:07:10 -0300 Subject: [PATCH 05/12] Add simple_form as development dependency It will not be "required" by default. So it doesn't increases gem's size. --- ransack.gemspec | 2 ++ spec/spec_helper.rb | 1 + 2 files changed, 3 insertions(+) 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/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' From 7be5edb678ddda5d52abfad4469160cf5110e23e Mon Sep 17 00:00:00 2001 From: "Abinoam P. Marques Jr" Date: Mon, 8 Apr 2024 11:08:49 -0300 Subject: [PATCH 06/12] Add railties. Required by simple_form. --- Gemfile | 3 +++ 1 file changed, 3 insertions(+) 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' From 4bd3ed4c743683b8760e28214bbf2f86a8e77caf Mon Sep 17 00:00:00 2001 From: "Abinoam P. Marques Jr" Date: Mon, 8 Apr 2024 11:09:10 -0300 Subject: [PATCH 07/12] Add a spec to check the default builder --- spec/ransack/helpers/form_helper_spec.rb | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/spec/ransack/helpers/form_helper_spec.rb b/spec/ransack/helpers/form_helper_spec.rb index e3c1655f..db88b838 100644 --- a/spec/ransack/helpers/form_helper_spec.rb +++ b/spec/ransack/helpers/form_helper_spec.rb @@ -857,6 +857,22 @@ module Helpers 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) {} } From ad9e36775e54d82b302371ac8aec48ada8cb0e41 Mon Sep 17 00:00:00 2001 From: "Abinoam P. Marques Jr" Date: Mon, 8 Apr 2024 13:55:01 -0300 Subject: [PATCH 08/12] Add SimpleFormBuilder spec It checks if SimpleForm#input method is callable. And checks for correct wrapping of a normal text field. --- .../helpers/simple_form_builder_spec.rb | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 spec/ransack/helpers/simple_form_builder_spec.rb 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..21825aec --- /dev/null +++ b/spec/ransack/helpers/simple_form_builder_spec.rb @@ -0,0 +1,55 @@ +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 + end + end + end +end From 1886b10c20aa265902d9242069ce24d3e51e580e Mon Sep 17 00:00:00 2001 From: "Abinoam P. Marques Jr" Date: Mon, 8 Apr 2024 23:45:52 -0300 Subject: [PATCH 09/12] Restore config so it doesn't mess with other specs --- spec/ransack/helpers/form_helper_spec.rb | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/spec/ransack/helpers/form_helper_spec.rb b/spec/ransack/helpers/form_helper_spec.rb index db88b838..43aee9e9 100644 --- a/spec/ransack/helpers/form_helper_spec.rb +++ b/spec/ransack/helpers/form_helper_spec.rb @@ -850,6 +850,11 @@ 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 } From 963337baa11756b111068d9e52f0a8a82d605d5b Mon Sep 17 00:00:00 2001 From: "Abinoam P. Marques Jr" Date: Sat, 13 Apr 2024 22:01:20 -0300 Subject: [PATCH 10/12] Add spec for 'type guessing' done by SimpleFormBuilder It should generate boolean fields whe the predicate is boolean. e.g.: _blank, _present, _true, _not_null It should respect the type of the ActiveRecord attribute otherwise. e.g.: Generate the proper date field for date attributes. --- .../helpers/simple_form_builder_spec.rb | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/spec/ransack/helpers/simple_form_builder_spec.rb b/spec/ransack/helpers/simple_form_builder_spec.rb index 21825aec..df009516 100644 --- a/spec/ransack/helpers/simple_form_builder_spec.rb +++ b/spec/ransack/helpers/simple_form_builder_spec.rb @@ -49,6 +49,34 @@ module Helpers 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 From 72e65f7234a5ae0a95388368a0225ca9d58a04ca Mon Sep 17 00:00:00 2001 From: "Abinoam P. Marques Jr" Date: Sat, 13 Apr 2024 22:04:18 -0300 Subject: [PATCH 11/12] Add methods for SimpleForm's attribute type inference Integrating with existing methods, even those not designated as 'public API', to minimize code duplication and maintain compatibility with future changes. --- lib/ransack/search.rb | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) 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) From 273e84ec5817048e31dd73e8054da61c4da80b7c Mon Sep 17 00:00:00 2001 From: "Abinoam P. Marques Jr" Date: Sat, 13 Apr 2024 22:55:51 -0300 Subject: [PATCH 12/12] Update docs about SimpleForm integration --- docs/docs/going-further/other-notes.md | 36 +++++++++++++++++++------- 1 file changed, 27 insertions(+), 9 deletions(-) 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.