stimulus_table_filter 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/CHANGELOG.md +16 -0
- data/Gemfile +17 -0
- data/Gemfile.lock +299 -0
- data/README.md +526 -0
- data/app/assets/javascripts/stimulus_table_filter/filter_list.js +17 -0
- data/app/assets/javascripts/stimulus_table_filter/page_navigator.js +25 -0
- data/app/assets/javascripts/stimulus_table_filter/paginator.js +29 -0
- data/app/assets/javascripts/stimulus_table_filter/row_matcher.js +36 -0
- data/app/assets/javascripts/stimulus_table_filter/row_sorter.js +33 -0
- data/app/assets/javascripts/stimulus_table_filter/row_stats.js +9 -0
- data/app/assets/javascripts/stimulus_table_filter/table_filter.js +75 -0
- data/app/assets/javascripts/stimulus_table_filter/table_filter_controller.js +234 -0
- data/app/assets/javascripts/stimulus_table_filter/table_filter_view.js +149 -0
- data/app/assets/javascripts/stimulus_table_filter/url_state.js +51 -0
- data/app/assets/stylesheets/stimulus_table_filter/table_filter.css +8 -0
- data/config/importmap.rb +2 -0
- data/eslint.config.js +18 -0
- data/lib/spec/shared_examples/table_filter_footer.rb +6 -0
- data/lib/spec/shared_examples/table_filter_rows.rb +13 -0
- data/lib/spec/shared_examples/table_filter_view.rb +87 -0
- data/lib/stimulus_table_filter/engine.rb +23 -0
- data/lib/stimulus_table_filter/error.rb +5 -0
- data/lib/stimulus_table_filter/rspec/helpers.rb +18 -0
- data/lib/stimulus_table_filter/rspec/matchers.rb +43 -0
- data/lib/stimulus_table_filter/rspec.rb +19 -0
- data/lib/stimulus_table_filter/version.rb +5 -0
- data/lib/stimulus_table_filter/view_helper/container.rb +28 -0
- data/lib/stimulus_table_filter/view_helper/controls.rb +42 -0
- data/lib/stimulus_table_filter/view_helper/rows.rb +32 -0
- data/lib/stimulus_table_filter/view_helper/sort.rb +31 -0
- data/lib/stimulus_table_filter/view_helper/stats.rb +47 -0
- data/lib/stimulus_table_filter/view_helper.rb +16 -0
- data/lib/stimulus_table_filter.rb +6 -0
- data/package-lock.json +1112 -0
- data/package.json +14 -0
- data/stimulus_table_filter.gemspec +29 -0
- data/test/javascript/controller.test.mjs +10 -0
- data/test/javascript/controller_stub.mjs +1 -0
- data/test/javascript/paginator.test.mjs +35 -0
- data/test/javascript/register.mjs +20 -0
- data/test/javascript/row_matcher.test.mjs +71 -0
- data/test/javascript/row_sorter.test.mjs +43 -0
- data/test/javascript/row_stats_and_url_state.test.mjs +69 -0
- data/test/javascript/stimulus_stub.mjs +6 -0
- metadata +107 -0
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../error'
|
|
4
|
+
|
|
5
|
+
module StimulusTableFilter
|
|
6
|
+
module RSpec
|
|
7
|
+
# Provides `html` for shared examples: `response.body` in request specs, `page.html` in system specs.
|
|
8
|
+
# Override with `let(:html) { ... }` in your example group if needed.
|
|
9
|
+
module Helpers
|
|
10
|
+
def html
|
|
11
|
+
return response.body if respond_to?(:response) && response.respond_to?(:body)
|
|
12
|
+
return page.html if respond_to?(:page) && page.respond_to?(:html)
|
|
13
|
+
|
|
14
|
+
raise StimulusTableFilter::Error, 'Define `let(:html) { ... }` or use inside a request or system spec'
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'nokogiri'
|
|
4
|
+
|
|
5
|
+
RSpec::Matchers.define :have_data do |name, value|
|
|
6
|
+
chain :on, :tag
|
|
7
|
+
|
|
8
|
+
match do |html|
|
|
9
|
+
values = data_values(html, name)
|
|
10
|
+
value.nil? ? values.any? : values.include?(value.to_s)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
description { value.nil? ? "have a data-#{name} attribute" : "have data-#{name}=\"#{value}\"" }
|
|
14
|
+
|
|
15
|
+
failure_message do |html|
|
|
16
|
+
found = data_values(html, name)
|
|
17
|
+
if found.empty?
|
|
18
|
+
"expected HTML to contain a data-#{name} attribute, but no element has one\n\nHTML:\n#{html}"
|
|
19
|
+
else
|
|
20
|
+
"expected HTML to contain data-#{name}=\"#{value}\", found: #{found.uniq.inspect}\n\nHTML:\n#{html}"
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def data_values(html, name)
|
|
25
|
+
Nokogiri::HTML.fragment(html.to_s).css(tag || '*').filter_map { |el| el["data-#{name}"] }
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
RSpec::Matchers.define :have_data_target do |target|
|
|
30
|
+
match do |html|
|
|
31
|
+
Nokogiri::HTML.fragment(html.to_s).css('*').any? do |el|
|
|
32
|
+
el['data-table-filter-target']&.split&.include?(target.to_s)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
description { "have an element with data-table-filter-target=\"#{target}\"" }
|
|
37
|
+
|
|
38
|
+
failure_message do |html|
|
|
39
|
+
present = Nokogiri::HTML.fragment(html.to_s).css('[data-table-filter-target]')
|
|
40
|
+
.filter_map { |el| el['data-table-filter-target'] }.uniq
|
|
41
|
+
"expected HTML to contain data-table-filter-target=\"#{target}\", targets present: #{present.inspect}"
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'rspec/helpers'
|
|
4
|
+
require_relative 'rspec/matchers'
|
|
5
|
+
require_relative '../spec/shared_examples/table_filter_view'
|
|
6
|
+
require_relative '../spec/shared_examples/table_filter_footer'
|
|
7
|
+
require_relative '../spec/shared_examples/table_filter_rows'
|
|
8
|
+
|
|
9
|
+
module StimulusTableFilter
|
|
10
|
+
# Opt-in wiring for the shared examples.
|
|
11
|
+
# Call StimulusTableFilter::RSpec.install! from your spec support file.
|
|
12
|
+
module RSpec
|
|
13
|
+
def self.install!
|
|
14
|
+
::RSpec.configure do |config|
|
|
15
|
+
config.include Helpers
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module StimulusTableFilter
|
|
4
|
+
module ViewHelper
|
|
5
|
+
def table_filter_container_tag(sort: 'name', dir: 'asc', page: nil, page_size: nil, url_key: nil,
|
|
6
|
+
debounce_ms: nil, tag: :div, **opts, &)
|
|
7
|
+
data = container_data(sort:, dir:, page:, page_size:, url_key:,
|
|
8
|
+
debounce_ms:).merge(opts.delete(:data) || {})
|
|
9
|
+
content_tag(tag, data: data, **opts, &)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
private
|
|
13
|
+
|
|
14
|
+
def base_container_data(sort:, dir:, page: nil)
|
|
15
|
+
base = { controller: 'table-filter', table_filter_sort_value: sort, table_filter_dir_value: dir }
|
|
16
|
+
base[:table_filter_page_value] = page.to_s if page
|
|
17
|
+
base
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def container_data(sort:, dir:, page: nil, page_size: nil, url_key: nil, debounce_ms: nil)
|
|
21
|
+
base = base_container_data(sort:, dir:, page:)
|
|
22
|
+
base = base.merge(table_filter_page_size_value: page_size.to_s) if page_size
|
|
23
|
+
base = base.merge(table_filter_url_key_value: url_key.to_s) if url_key
|
|
24
|
+
base = base.merge(table_filter_debounce_ms_value: debounce_ms.to_s) if debounce_ms
|
|
25
|
+
base
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module StimulusTableFilter
|
|
4
|
+
module ViewHelper
|
|
5
|
+
def table_filter_search_tag(**opts) = tag.input(type: 'search', data: target_data('search', opts), **opts)
|
|
6
|
+
|
|
7
|
+
def table_filter_filter_btn_tag(value, label, dimension: nil, **opts)
|
|
8
|
+
data = with_data({ filter_btn: value }, opts)
|
|
9
|
+
data[:filter_dimension] = dimension if dimension
|
|
10
|
+
content_tag(:button, label, data: data, **opts)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def table_filter_empty_row_tag(**opts, &)
|
|
14
|
+
data = target_data('emptyRow', opts)
|
|
15
|
+
klass = ['hidden', *Array(opts.delete(:class))].join(' ')
|
|
16
|
+
content_tag(:tr, data: data, class: klass, **opts, &)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def table_filter_active_filter_tag(dimension: nil, **opts, &)
|
|
20
|
+
data = target_data('filterDisplay', opts)
|
|
21
|
+
data[:filter_dimension] = dimension if dimension
|
|
22
|
+
content_tag(:span, data: data, hidden: true, **opts, &)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def table_filter_select_tag(options, dimension: nil, **opts)
|
|
26
|
+
data = with_data({ table_filter_target: 'filterSelect' }, opts)
|
|
27
|
+
data[:filter_dimension] = dimension if dimension
|
|
28
|
+
pairs = normalize_options(options)
|
|
29
|
+
content_tag(:select, data: data, **opts) { render_options(pairs) }
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def option_tag(pair) = content_tag(:option, pair.first, value: pair.last)
|
|
35
|
+
def render_options(pairs) = safe_join(pairs.map(&method(:option_tag)))
|
|
36
|
+
|
|
37
|
+
def normalize_options(options)
|
|
38
|
+
pairs = options.is_a?(Hash) ? options.to_a : options
|
|
39
|
+
pairs.map { |pair| pair.is_a?(Array) ? pair : [pair, pair] }
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module StimulusTableFilter
|
|
4
|
+
module ViewHelper
|
|
5
|
+
def table_filter_row_attrs(name:, filters: {}, sort: {}, searchable: nil, group: nil)
|
|
6
|
+
data = { table_filter_target: 'row', name: name.to_s.downcase, **filter_data(filters), **sort_data(sort) }
|
|
7
|
+
extras = { searchable: searchable&.to_s&.downcase, group: group&.to_s }.compact
|
|
8
|
+
{ data: data.merge(extras) }
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def table_filter_row_tag(name:, filters: {}, sort: {}, searchable: nil, group: nil,
|
|
12
|
+
tag: :tr, **html_options, &)
|
|
13
|
+
data = with_data(
|
|
14
|
+
table_filter_row_attrs(name: name, filters: filters, sort: sort,
|
|
15
|
+
searchable: searchable, group: group)[:data], html_options
|
|
16
|
+
)
|
|
17
|
+
content_tag(tag, data: data, **html_options, &)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def table_filter_group_header_tag(group, tag: :tr, **opts, &)
|
|
21
|
+
data = with_data({ table_filter_target: 'groupHeader', group: group.to_s }, opts)
|
|
22
|
+
content_tag(tag, data: data, **opts, &)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def filter_key(key) = :"filter_#{key}"
|
|
28
|
+
def filter_data(filters) = filters.transform_keys(&method(:filter_key)).transform_values(&:to_s)
|
|
29
|
+
def sort_key(key) = :"sort_#{key}"
|
|
30
|
+
def sort_data(sort) = sort.transform_keys(&method(:sort_key)).transform_values(&:to_s)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module StimulusTableFilter
|
|
4
|
+
module ViewHelper
|
|
5
|
+
def table_filter_sort_btn_tag(col, label, type: nil, **)
|
|
6
|
+
table_filter_build_sort_trigger(:button, col, label, type: type, **)
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def table_filter_sort_th_tag(col, label, type: nil, **)
|
|
10
|
+
table_filter_build_sort_trigger(:th, col, label, type: type, **)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def table_filter_column_tag(col:, label:, type: nil, sortable: true, **)
|
|
14
|
+
return content_tag(:th, label, scope: 'col', **) unless sortable
|
|
15
|
+
|
|
16
|
+
table_filter_sort_th_tag(col, label, type: type, **)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
def sort_trigger_icon = content_tag(:span, '', data: { sort_icon: '' })
|
|
22
|
+
def sort_btn_base(col, type) = type ? { sort_btn: col, sort_type: type } : { sort_btn: col }
|
|
23
|
+
def sort_trigger_data(col, type, opts) = sort_btn_base(col, type).merge(opts.delete(:data) || {})
|
|
24
|
+
|
|
25
|
+
def table_filter_build_sort_trigger(tag, col, label, type: nil, **opts)
|
|
26
|
+
data = sort_trigger_data(col, type, opts)
|
|
27
|
+
opts = { scope: 'col' }.merge(opts) if tag == :th
|
|
28
|
+
content_tag(tag, ERB::Util.html_escape(label) + sort_trigger_icon, data: data, **opts)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module StimulusTableFilter
|
|
4
|
+
module ViewHelper
|
|
5
|
+
def table_filter_prev_btn_tag(label = '←', **opts)
|
|
6
|
+
content_tag(:button, label, data: with_data({ prev_page: '', table_filter_target: 'prevPage' }, opts), **opts)
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def table_filter_next_btn_tag(label = '→', **opts)
|
|
10
|
+
content_tag(:button, label, data: with_data({ next_page: '', table_filter_target: 'nextPage' }, opts), **opts)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def table_filter_page_info_tag(**opts) = content_tag(:span, '', data: target_data('pageInfo', opts), **opts)
|
|
14
|
+
|
|
15
|
+
def table_filter_match_count_tag(**) = table_filter_stat_span('matchCount', **)
|
|
16
|
+
def table_filter_total_count_tag(**) = table_filter_stat_span('totalCount', **)
|
|
17
|
+
def table_filter_match_pct_tag(**) = table_filter_stat_span('matchPct', **)
|
|
18
|
+
|
|
19
|
+
def table_filter_count_tag(dimension:, value:, **opts)
|
|
20
|
+
data = with_data({ count_dimension: dimension, count_value: value }, opts)
|
|
21
|
+
content_tag(:span, '', data: data, **opts)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def table_filter_footer_tag(colspan: nil, tag: :tfoot, **opts, &)
|
|
25
|
+
content_tag(tag) do
|
|
26
|
+
content_tag(:tr) do
|
|
27
|
+
td_opts = colspan ? { colspan: colspan } : {}
|
|
28
|
+
content_tag(:td, **td_opts, **opts) { footer_content(&) }
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def footer_content(&)
|
|
36
|
+
return default_footer_content unless block_given?
|
|
37
|
+
|
|
38
|
+
capture(table_filter_match_count_tag, table_filter_total_count_tag, &)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def table_filter_stat_span(target, **opts) = content_tag(:span, '', data: target_data(target, opts), **opts)
|
|
42
|
+
|
|
43
|
+
def default_footer_content
|
|
44
|
+
safe_join([table_filter_match_count_tag, ' of ', table_filter_total_count_tag])
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module StimulusTableFilter
|
|
4
|
+
module ViewHelper
|
|
5
|
+
private
|
|
6
|
+
|
|
7
|
+
def with_data(base, opts) = base.merge(opts.delete(:data) || {})
|
|
8
|
+
def target_data(target, opts) = with_data({ table_filter_target: target }, opts)
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
require_relative 'view_helper/container'
|
|
13
|
+
require_relative 'view_helper/rows'
|
|
14
|
+
require_relative 'view_helper/controls'
|
|
15
|
+
require_relative 'view_helper/sort'
|
|
16
|
+
require_relative 'view_helper/stats'
|