Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Simple form integration #1487

Open
wants to merge 12 commits into
base: main
Choose a base branch
from
3 changes: 3 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -27,19 +27,22 @@ 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
gem 'activesupport'
gem 'activemodel'
gem 'activerecord', require: false
gem 'actionpack'
gem 'railties'
end
end
gem 'mysql2'
Expand Down
36 changes: 27 additions & 9 deletions docs/docs/going-further/other-notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
1 change: 1 addition & 0 deletions lib/ransack/helpers.rb
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
require 'ransack/helpers/form_builder'
require 'ransack/helpers/simple_form_builder' if defined?(::SimpleForm)
require 'ransack/helpers/form_helper'
9 changes: 6 additions & 3 deletions lib/ransack/helpers/form_builder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
13 changes: 13 additions & 0 deletions lib/ransack/helpers/form_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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') %>
Expand Down
18 changes: 18 additions & 0 deletions lib/ransack/helpers/simple_form_builder.rb
Original file line number Diff line number Diff line change
@@ -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
35 changes: 35 additions & 0 deletions lib/ransack/search.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 2 additions & 0 deletions ransack.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -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) }
Expand Down
27 changes: 27 additions & 0 deletions spec/ransack/helpers/form_helper_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
83 changes: 83 additions & 0 deletions spec/ransack/helpers/simple_form_builder_spec.rb
Original file line number Diff line number Diff line change
@@ -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.*?><label.*?<\/label>.*?<input.*?\/>.*?<\/div>/)
end

it "the wrapping div should have class 'q_name_cont'" do
expect(subject).to match(/<div.*?class=".*?q_name_cont.*?".*?>/)
end

it "should generate correct label text with predicate from locale files" do
expect(subject).to match(/<label.*?>.*?Full Name contains.*?<\/label>/)
end

it "should generate correct input name=\"q[name_cont]\"" do
expect(subject).to match(/<input.*?name="q\[name_cont\]".*?\/>/)
end

it "should generate correct input id=\"q_name_cont\"" do
expect(subject).to match(/<input.*?id="q_name_cont".*?\/>/)
end

it "should generate correct input type=\"text\"" do
expect(subject).to match(/<input.*?type="text".*?\/>/)
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(/<div.*?class=".*?boolean.*?".*?>/)
end

it "should generate correct input type=\"checkbox\"" do
expect(subject).to match(/<input.*?type="checkbox".*?\/>/)
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(/<div.*?class=".*?input date.*?".*?>/)
end

it "should generate selects for the fields of the date (year, month, day), at least" do
expect(subject).to match(/<select.*?name="q\[life_start_gteq\(1i\)\]".*?>/)
expect(subject).to match(/<select.*?name="q\[life_start_gteq\(2i\)\]".*?>/)
expect(subject).to match(/<select.*?name="q\[life_start_gteq\(3i\)\]".*?>/)
end
end
end
end
end
end
end
1 change: 1 addition & 0 deletions spec/spec_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
require 'faker'
require 'ransack'
require 'action_controller'
require 'simple_form'
require 'ransack/helpers'
require 'pry'
require 'simplecov'
Expand Down