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.
@@ -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
@@ -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).
@@ -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
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ SearchAutocomplete::Engine.routes.draw do
4
+ get '/*model_name', to: 'autocomplete#autocomplete', as: 'search'
5
+ end
@@ -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,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SearchAutocomplete
4
+ # Autocomplete engine
5
+ class Engine < ::Rails::Engine
6
+ engine_name 'search_autocomplete'
7
+ isolate_namespace SearchAutocomplete
8
+ end
9
+ 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,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SearchAutocomplete
4
+ # Configuration module
5
+ module Options
6
+ mattr_accessor :autocomplete_size
7
+
8
+ def configure
9
+ yield self
10
+ end
11
+ end
12
+ 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
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SearchAutocomplete
4
+ # Gem version
5
+ VERSION = '0.1.0'
6
+ 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: []