search_autocomplete 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/README.md +132 -0
- data/Rakefile +34 -0
- data/app/controllers/search_autocomplete/autocomplete_controller.rb +63 -0
- data/config/routes.rb +5 -0
- data/lib/search_autocomplete.rb +15 -0
- data/lib/search_autocomplete/autocompletable.rb +32 -0
- data/lib/search_autocomplete/engine.rb +9 -0
- data/lib/search_autocomplete/form_builder_helper.rb +33 -0
- data/lib/search_autocomplete/options.rb +12 -0
- data/lib/search_autocomplete/searchable.rb +62 -0
- data/lib/search_autocomplete/version.rb +6 -0
- metadata +88 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 9c795f6eacd1ecd7cdba955188e9250b5e0a2b8e0d208742ffd41ea78cd7a380
|
4
|
+
data.tar.gz: e47d9e1270cea9d83343213f31f3ccf00040d761df2f2113630d24e2dd93acd1
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: d344fddbe8b93c3cbd31fbeb5c260a69a2b9a3c9fea650fc8f0717947d6c7b9ec1d86317860ab6eee70603d435cbaf032a897a703bad82a53f7d12058336b69f
|
7
|
+
data.tar.gz: ba277717530d72f6a7737c2d0fe816a20707687022a2cbc537aca8b7222b111dc260acf81f5f6f4716397d164348e7a38cca8f17ad3422a920a0cca2c2755b25
|
data/README.md
ADDED
@@ -0,0 +1,132 @@
|
|
1
|
+
# SearchAutocomplete
|
2
|
+
This gem was created to add simple autocomplete and search/filter functionality to Ruby on Rails apps with minimal effort.
|
3
|
+
|
4
|
+
Other alternatives available are outdated or aren't as simple, often requiring many external libraries such as jQuery.
|
5
|
+
This gem only requires modules already shipped with Rails and the only external library required is available as a npm package you can add in your webpack files.
|
6
|
+
|
7
|
+
## Installation
|
8
|
+
|
9
|
+
### Ruby
|
10
|
+
Add this line to your application's Gemfile:
|
11
|
+
|
12
|
+
```ruby
|
13
|
+
gem 'search_autocomplete'
|
14
|
+
```
|
15
|
+
|
16
|
+
And then execute:
|
17
|
+
```bash
|
18
|
+
$ bundle
|
19
|
+
```
|
20
|
+
|
21
|
+
Or install it yourself as:
|
22
|
+
```bash
|
23
|
+
$ gem install search_autocomplete
|
24
|
+
```
|
25
|
+
|
26
|
+
### Webpack
|
27
|
+
|
28
|
+
Add the compatible web component for npm:
|
29
|
+
```bash
|
30
|
+
yarn add @francisschiavo/search-autocomplete
|
31
|
+
```
|
32
|
+
|
33
|
+
and then require it anywhere in your scripts:
|
34
|
+
|
35
|
+
```js
|
36
|
+
require('@francisschiavo/search-autocomplete');
|
37
|
+
```
|
38
|
+
|
39
|
+
## Usage
|
40
|
+
|
41
|
+
### Autocomplete
|
42
|
+
|
43
|
+
After installing this gem you can configure its options with an initializer like this:
|
44
|
+
|
45
|
+
**config/initializers/search_autocomplete.rb**
|
46
|
+
```ruby
|
47
|
+
SearchAutocomplete.configure do |options|
|
48
|
+
options.autocomplete_size = 5
|
49
|
+
end
|
50
|
+
```
|
51
|
+
|
52
|
+
You must mount the autocomplete engine like this:
|
53
|
+
|
54
|
+
**config/routes.rb**
|
55
|
+
```ruby
|
56
|
+
Rails.application.routes.draw do
|
57
|
+
mount SearchAutocomplete::Engine, at: '/autocomplete'
|
58
|
+
end
|
59
|
+
```
|
60
|
+
|
61
|
+
To allow your model on the autocomplete search you must configure it like this:
|
62
|
+
```ruby
|
63
|
+
class Category < ApplicationRecord
|
64
|
+
belongs_to :category, optional: true
|
65
|
+
|
66
|
+
autocomplete :name
|
67
|
+
end
|
68
|
+
```
|
69
|
+
The example above will allow searches on the `name` field for the `Category` model.
|
70
|
+
|
71
|
+
To test this you can do a get request to: `{APP URL}/autocomplete/category?term=Cat 1`
|
72
|
+
|
73
|
+
Note `/autocomplete` is the base uri you specify on the routes and `/category` is the lowercase name of the model.
|
74
|
+
|
75
|
+
You can also search namespaced models by adding the lowercase name of every namespace as part of the URI:
|
76
|
+
|
77
|
+
If your model is `Admin::User` the search would be: `{APP URL}/autocomplete/admin/user?term=Roger`
|
78
|
+
|
79
|
+
### Search / Filter
|
80
|
+
|
81
|
+
You can use the method `search` on your controllers as a way to filter data.
|
82
|
+
|
83
|
+
This method takes 3 arguments:
|
84
|
+
* The model class to perform the search
|
85
|
+
* An array of fields to permit approximate matches (like)
|
86
|
+
* An array of fields to permit exact matches (=)
|
87
|
+
|
88
|
+
Here is a sample of the `index` action of a `category` controller:
|
89
|
+
|
90
|
+
```ruby
|
91
|
+
def index
|
92
|
+
@categories = search(Category, %i[name], []).all
|
93
|
+
end
|
94
|
+
```
|
95
|
+
|
96
|
+
You can also use it with pagination gems like kaminari:
|
97
|
+
|
98
|
+
```ruby
|
99
|
+
def index
|
100
|
+
@categories = search(Category, %i[name], []).page(params[:page])
|
101
|
+
end
|
102
|
+
```
|
103
|
+
|
104
|
+
### Postgres Jsonb support
|
105
|
+
|
106
|
+
There is a limited support for `jsonb` fields, currently limited to one level deep fields.
|
107
|
+
|
108
|
+
To query using a jsonb field you must pass an array as the name argument:
|
109
|
+
|
110
|
+
```ruby
|
111
|
+
class Category < ApplicationRecord
|
112
|
+
belongs_to :category, optional: true
|
113
|
+
|
114
|
+
autocomplete %i[name pt_BR]
|
115
|
+
end
|
116
|
+
```
|
117
|
+
|
118
|
+
This will use `arel` infix operators to create the following query:
|
119
|
+
|
120
|
+
```sql
|
121
|
+
SELECT * FROM categories WHERE category.name->>'pt_BR' ILIKE "%term%";
|
122
|
+
```
|
123
|
+
|
124
|
+
## Known issues
|
125
|
+
|
126
|
+
* Uses WebComponents requires modern browsers or polyfills
|
127
|
+
* There is no automated tests at this moment.
|
128
|
+
* There is no support for json fields other than `postgres`.
|
129
|
+
* Json fields are supported but limited to one level deep fields.
|
130
|
+
|
131
|
+
## License
|
132
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
begin
|
4
|
+
require 'bundler/setup'
|
5
|
+
rescue LoadError
|
6
|
+
puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
|
7
|
+
end
|
8
|
+
|
9
|
+
require 'rdoc/task'
|
10
|
+
|
11
|
+
RDoc::Task.new(:rdoc) do |rdoc|
|
12
|
+
rdoc.rdoc_dir = 'rdoc'
|
13
|
+
rdoc.title = 'SearchAutocomplete'
|
14
|
+
rdoc.options << '--line-numbers'
|
15
|
+
rdoc.rdoc_files.include('README.md')
|
16
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
17
|
+
end
|
18
|
+
|
19
|
+
APP_RAKEFILE = File.expand_path('test/dummy/Rakefile', __dir__)
|
20
|
+
load 'rails/tasks/engine.rake'
|
21
|
+
|
22
|
+
load 'rails/tasks/statistics.rake'
|
23
|
+
|
24
|
+
require 'bundler/gem_tasks'
|
25
|
+
|
26
|
+
require 'rake/testtask'
|
27
|
+
|
28
|
+
Rake::TestTask.new(:test) do |t|
|
29
|
+
t.libs << 'test'
|
30
|
+
t.pattern = 'test/**/*_test.rb'
|
31
|
+
t.verbose = false
|
32
|
+
end
|
33
|
+
|
34
|
+
task default: :test
|
@@ -0,0 +1,63 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SearchAutocomplete
|
4
|
+
# Autocomplete
|
5
|
+
class AutocompleteController < ActionController::Base
|
6
|
+
before_action :find_model, only: :autocomplete
|
7
|
+
|
8
|
+
##
|
9
|
+
# Main autocomplete action
|
10
|
+
def autocomplete
|
11
|
+
data = autocomplete_search
|
12
|
+
render json: data.map { |item| { id: item.id, label: label(item), value: value(item) } }
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
def autocomplete_search
|
18
|
+
arel_table = @model.arel_table
|
19
|
+
node = Arel::Nodes::SqlLiteral.new "'%#{params[:term]}%'"
|
20
|
+
|
21
|
+
search_field = @model.autocomplete_options[:search_field]
|
22
|
+
# Assume postgres jsonb
|
23
|
+
if search_field.is_a? Array
|
24
|
+
op = Arel::Nodes::InfixOperation.new('->>', arel_table[search_field[0]], Arel::Nodes.build_quoted(search_field[1]))
|
25
|
+
query_builder = params[:term] ? @model.where(op.matches(node)) : @model
|
26
|
+
else
|
27
|
+
query_builder = params[:term] ? @model.where(arel_table[search_field].matches(node)) : @model
|
28
|
+
end
|
29
|
+
|
30
|
+
@model.autocomplete_options[:filters].each do |filter|
|
31
|
+
next unless params[filter.to_s].present?
|
32
|
+
|
33
|
+
query_builder = query_builder.where(arel_table[filter].eq(params[filter.to_s]))
|
34
|
+
end
|
35
|
+
query_builder.limit(SearchAutocomplete.autocomplete_size)
|
36
|
+
end
|
37
|
+
|
38
|
+
def label(item)
|
39
|
+
values = @model.autocomplete_options[:display_fields].map do |field|
|
40
|
+
if field.is_a? Array
|
41
|
+
item.instance_eval(field[0].to_s)[field[1].to_s]
|
42
|
+
else
|
43
|
+
item.instance_eval(field.to_s)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
values.join ' - '
|
47
|
+
end
|
48
|
+
|
49
|
+
def value(item)
|
50
|
+
search_field = @model.autocomplete_options[:search_field]
|
51
|
+
if search_field.is_a? Array
|
52
|
+
item.instance_eval(search_field[0].to_s)[search_field[1].to_s]
|
53
|
+
else
|
54
|
+
item.instance_eval(search_field.to_s)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def find_model
|
59
|
+
model_name = params[:model_name].sub('/', '::').camelize
|
60
|
+
@model = model_name.constantize
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
data/config/routes.rb
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'search_autocomplete/engine'
|
4
|
+
require 'search_autocomplete/options'
|
5
|
+
|
6
|
+
# Provides an easy way to add autocomplete and filters to rails apps
|
7
|
+
module SearchAutocomplete
|
8
|
+
extend SearchAutocomplete::Options
|
9
|
+
end
|
10
|
+
|
11
|
+
require 'search_autocomplete/autocompletable'
|
12
|
+
require 'search_autocomplete/searchable'
|
13
|
+
require 'search_autocomplete/form_builder_helper'
|
14
|
+
|
15
|
+
::ActiveRecord::Base.include SearchAutocomplete::Autocompletable
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SearchAutocomplete
|
4
|
+
# Autocompletable
|
5
|
+
module Autocompletable
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
|
8
|
+
included do
|
9
|
+
cattr_accessor :autocomplete_options
|
10
|
+
self.autocomplete_options = { configured: false }
|
11
|
+
end
|
12
|
+
|
13
|
+
# Autocompletable methods
|
14
|
+
module ClassMethods
|
15
|
+
##
|
16
|
+
# Configures this model to respond to autocomplete searches
|
17
|
+
#
|
18
|
+
# @param search_field [String|Array] Name of the main field to perform the search. If an array is given it will search in a jsonb structure.
|
19
|
+
# @param display_fields [Array{Symbol}] Array of field names for concatenating as display result
|
20
|
+
# @param filters [Array{Symbol}] Array of additional fields to filter
|
21
|
+
#
|
22
|
+
def autocomplete(search_field, display_fields = [], filters = [])
|
23
|
+
self.autocomplete_options = {
|
24
|
+
configured: true,
|
25
|
+
search_field: search_field,
|
26
|
+
display_fields: display_fields.size.zero? ? [search_field] : display_fields,
|
27
|
+
filters: filters
|
28
|
+
}
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Inherited from Rails
|
4
|
+
module ActionView
|
5
|
+
# Inherited from Rails
|
6
|
+
module Helpers
|
7
|
+
# Helper method for form builder
|
8
|
+
class FormBuilder
|
9
|
+
##
|
10
|
+
# Creates an autocomplete element from form builder
|
11
|
+
def autocomplete_field(method, display_value, autocomplete_path, options = {})
|
12
|
+
options.reverse_merge!(
|
13
|
+
'display-value': find_autocomplete_value(display_value),
|
14
|
+
value: @object.send(method),
|
15
|
+
url: autocomplete_path,
|
16
|
+
minlength: 2,
|
17
|
+
name: "#{@object_name}[#{method}]"
|
18
|
+
)
|
19
|
+
|
20
|
+
autocomplete_options[:autofocus] = options[:autofocus] if options.include? :autofocus
|
21
|
+
@template.content_tag(:'auto-complete', nil, options)
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def find_autocomplete_value(display_expression)
|
27
|
+
@object.instance_eval(display_expression)
|
28
|
+
rescue StandardError
|
29
|
+
''
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Inherited from Rails
|
4
|
+
module ActionController
|
5
|
+
# Searchable
|
6
|
+
class Base
|
7
|
+
##
|
8
|
+
# Performs a search on the model based on permitted fields
|
9
|
+
#
|
10
|
+
# @param model [Class] Model to perform the search
|
11
|
+
# @param approximate_fields [Array{Symbol}] List of fields to allow as approximate filters
|
12
|
+
# @param exact_fields [Array{Symbol}] List of fields to allow as exact filters
|
13
|
+
# @param include_list [Array{Symbol}] List of related resources to include
|
14
|
+
def search(model, approximate_fields = [], exact_fields = [], include_list = nil)
|
15
|
+
arel_table = model.arel_table
|
16
|
+
|
17
|
+
search_conditions = prepare_search_fields arel_table, exact_fields
|
18
|
+
search_conditions += prepare_search_fields(arel_table, approximate_fields, false)
|
19
|
+
|
20
|
+
query = include_list.present? ? model.includes(include_list) : model
|
21
|
+
query = query.where(*search_conditions) if search_conditions.length.positive?
|
22
|
+
query
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def prepare_search_fields(table, fields, exact = true)
|
28
|
+
search_conditions = []
|
29
|
+
fields.each do |field|
|
30
|
+
if field.is_a?(Array)
|
31
|
+
search_conditions.push prepare_jsonb_condition(table, field, exact)
|
32
|
+
else
|
33
|
+
search_conditions.push prepare_simple_condition(table, field, exact)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
search_conditions
|
37
|
+
end
|
38
|
+
|
39
|
+
def prepare_simple_condition(table, field, exact)
|
40
|
+
return nil unless params.key? field
|
41
|
+
|
42
|
+
if exact
|
43
|
+
table[field].eq(params[field])
|
44
|
+
else
|
45
|
+
table[field].matches(Arel::Nodes::SqlLiteral.new("'%#{params[field]}%'"))
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def prepare_jsonb_condition(table, field, exact)
|
50
|
+
field_name = field[0].to_s
|
51
|
+
return nil unless params.key? field_name
|
52
|
+
|
53
|
+
condition = Arel::Nodes::InfixOperation.new('->>', table[field_name], Arel::Nodes.build_quoted(field[1]))
|
54
|
+
|
55
|
+
if exact
|
56
|
+
condition.eq(params[field_name])
|
57
|
+
else
|
58
|
+
condition.matches(Arel::Nodes::SqlLiteral.new("'%#{params[field_name]}%'"))
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
metadata
ADDED
@@ -0,0 +1,88 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: search_autocomplete
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Francis Schiavo
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2020-06-11 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rails
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 6.0.0
|
20
|
+
- - ">="
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: 6.0.0.0
|
23
|
+
type: :runtime
|
24
|
+
prerelease: false
|
25
|
+
version_requirements: !ruby/object:Gem::Requirement
|
26
|
+
requirements:
|
27
|
+
- - "~>"
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: 6.0.0
|
30
|
+
- - ">="
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: 6.0.0.0
|
33
|
+
- !ruby/object:Gem::Dependency
|
34
|
+
name: sqlite3
|
35
|
+
requirement: !ruby/object:Gem::Requirement
|
36
|
+
requirements:
|
37
|
+
- - "~>"
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: 1.4.2
|
40
|
+
type: :runtime
|
41
|
+
prerelease: false
|
42
|
+
version_requirements: !ruby/object:Gem::Requirement
|
43
|
+
requirements:
|
44
|
+
- - "~>"
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: 1.4.2
|
47
|
+
description: Search and autocomplete based on Arel and WebComponents.
|
48
|
+
email:
|
49
|
+
- francischiavo@gmail.com
|
50
|
+
executables: []
|
51
|
+
extensions: []
|
52
|
+
extra_rdoc_files: []
|
53
|
+
files:
|
54
|
+
- README.md
|
55
|
+
- Rakefile
|
56
|
+
- app/controllers/search_autocomplete/autocomplete_controller.rb
|
57
|
+
- config/routes.rb
|
58
|
+
- lib/search_autocomplete.rb
|
59
|
+
- lib/search_autocomplete/autocompletable.rb
|
60
|
+
- lib/search_autocomplete/engine.rb
|
61
|
+
- lib/search_autocomplete/form_builder_helper.rb
|
62
|
+
- lib/search_autocomplete/options.rb
|
63
|
+
- lib/search_autocomplete/searchable.rb
|
64
|
+
- lib/search_autocomplete/version.rb
|
65
|
+
homepage: https://github.com/francis-schiavo/search_autocomplete
|
66
|
+
licenses:
|
67
|
+
- MIT
|
68
|
+
metadata: {}
|
69
|
+
post_install_message:
|
70
|
+
rdoc_options: []
|
71
|
+
require_paths:
|
72
|
+
- lib
|
73
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
74
|
+
requirements:
|
75
|
+
- - ">="
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: '0'
|
78
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
requirements: []
|
84
|
+
rubygems_version: 3.1.2
|
85
|
+
signing_key:
|
86
|
+
specification_version: 4
|
87
|
+
summary: This gem adds autocomplete and filter functionality to rails apps.
|
88
|
+
test_files: []
|