hotwire_combobox 0.1.5 → 0.1.7

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 99cf62ece0c5bf6182ab759cbab18c4f561c22215babfdf00eed7329f5797eb9
4
- data.tar.gz: dcca7ddceca4e3b7f840716bf789b641d62ffaea589b9bf86da39e4445c6a9cf
3
+ metadata.gz: 01a08bf7fdf486660daebfd29960a62c8435ad87939a8963d805967872ffca4d
4
+ data.tar.gz: 2ca73e1f2693d9adf838463187bd2c79867b8086d59beea0bb699c12a49e8507
5
5
  SHA512:
6
- metadata.gz: fb5f896deae2dde8b36d91d7eaee8ec27abfd00b07a94d64c094d8da865e4cd2723c6349a9fe49296f8e20cde933ed698f2f64b1e65909ed1fca889517213773
7
- data.tar.gz: 31ce6c44d91feb634fd16349ed0f89ab58975a3a6744f36c6f05f6ea207d6caa1317267b1892ae602b48daf5f4182a3908681c7746e05bd19ae9b489bcf9696b
6
+ metadata.gz: d505fbfb95060cb730a5533dd4d850f4691391a94f4849fd12017f1ebca12df55a12871d29f784e55dccbe2a8512c007c7516f960225e1a13f6ab74d5d8a3431
7
+ data.tar.gz: 6792bae8acf5f9051f2a5605f95e86d7e3b755d323e47e9dff65dc0a02eb27771fa145d2129b246518711e4b050cc2e3cba8b03e83b581ea519c31c30a2ad5d6
data/README.md CHANGED
@@ -1,14 +1,15 @@
1
- # Hotwire Combobox
2
- Short description and motivation.
1
+ # HotwireCombobox
3
2
 
4
- ## Usage
5
- How to use my plugin.
3
+ A combobox implementation for Ruby on Rails apps running on Hotwire.
4
+
5
+ > [!WARNING]
6
+ > This gem is pre-release software. It's not ready for production use yet and the API is subject to change.
6
7
 
7
8
  ## Installation
8
9
  Add this line to your application's Gemfile:
9
10
 
10
11
  ```ruby
11
- gem "hotwire_combobox", require: "combobox"
12
+ gem "hotwire_combobox"
12
13
  ```
13
14
 
14
15
  And then execute:
@@ -16,11 +17,97 @@ And then execute:
16
17
  $ bundle
17
18
  ```
18
19
 
19
- Or install it yourself as:
20
- ```bash
21
- $ gem install hotwire_combobox
20
+ ## Output
21
+
22
+ This is the stripped-down output of a combobox generated with HotwireCombobox. Understanding it will be helpful in getting the most out of this library.
23
+
24
+ ```html
25
+ <fieldset class="hw-combobox">
26
+ <input type="hidden" name="provided_name">
27
+
28
+ <input type="text" role="combobox">
29
+
30
+ <ul role="listbox">
31
+ <li role="option">Provided Option 1 Content</li>
32
+ <li role="option">Provided Option 2 Content</li>
33
+ <!-- ... -->
34
+ </ul>
35
+ </fieldset>
36
+ ```
37
+
38
+ The `<ul role="listbox">` element is what gets shown when the combobox is open.
39
+
40
+ The library uses stimulus to add interactivity to the combobox and sync the input element's value with the hidden input element.
41
+
42
+ The hidden input's value is what ultimately gets sent to the server when submitting a form containing the combobox.
43
+
44
+ ## Usage
45
+
46
+ ### Options
47
+
48
+ Options are what you see when you open the combobox.
49
+
50
+ The `options` argument takes an array of any objects which respond to:
51
+
52
+ | Attribute | Description | Required? |
53
+ |----------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------|-------------------|
54
+ | `id` | Used as the option element's `id` attribute. Only required if `value` is not provided. | Required* |
55
+ | `value` | Used to populate the input element's `value` attribute. Falls back to calling `id` on the object if not provided. | Optional |
56
+ | `content` <br> **Supports HTML** | Used as the option element's content. Falls back to calling `display` on the object if not provided. | Optional |
57
+ | `filterable_as` | Used to filter down the options when the user types into the input element. Falls back to calling `display` on the object if not provided. | Optional |
58
+ | `autocompletable_as` | Used to autocomplete the input element when the user types into it. Falls back to calling `display` on the object if not provided. | Optional |
59
+ | `display` | Used as a short-hand for other attributes. See the rest of the list for details. | Optional |
60
+
61
+
62
+ > [!NOTE]
63
+ > The `id` attribute is required only if `value` is not provided.
64
+
65
+
66
+ The gem provides a `HotwireCombobox::Option` class which you can use to create options:
67
+
68
+ ```ruby
69
+ @states = [
70
+ HotwireCombobox::Option.new(value: "AL", display: "Alabama"),
71
+ # ...
72
+ ]
73
+ ```
74
+
75
+ If you feel `HotwireCombobox::Option` is too verbose, you can also use the `hwbox_options` helper. It will destructure the hashes you pass to it and create `HotwireCombobox::Option` instances for you:
76
+
77
+ ```ruby
78
+ @states = hwbox_options [
79
+ { value: "AL", display: "Alabama" },
80
+ # ...
81
+ ]
22
82
  ```
23
83
 
84
+ ### Styling
85
+
86
+ The combobox is completely unstyled by default. You can use the following CSS selectors to style the combobox:
87
+
88
+ * `.hw-combobox` targets the `<fieldset>` element used to wrap the whole component.
89
+ * `.hw-combobox [role="combobox"]` targets the input element.
90
+ * `.hw-combobox [role="listbox"]` targets the listbox which encloses all option elements.
91
+ * `.hw-combobox [role="option"]` targets each option element inside the listbox.
92
+
93
+ Additionally, you can pass the following [Stimulus class values](https://stimulus.hotwired.dev/reference/css-classes) to `hw_combobox_tag`:
94
+
95
+ * `data-hw-combobox-selected-class`: The class to apply to the selected option while shown inside an open listbox.
96
+ * `data-hw-combobox-invalid-class`: The class to apply to the input element when the current value is invalid.
97
+
98
+ ### Validity
99
+
100
+ The hidden input can't have a value that's not in the list of options.
101
+
102
+ If a nonexistent value is typed into the combobox, the value of the hidden input will be set empty.
103
+
104
+ The only way a value can be marked as invalid is if the field is required and empty after having interacted with the combobox.
105
+
106
+ The library will mark the element as invalid but this won't be noticeable in the UI unless you specify styles for the invalid state. See the [Styling](#styling) section for details.
107
+
108
+ > [!CAUTION]
109
+ > Bad actors can still submit invalid values to the server. You should always validate the input on the server side.
110
+
24
111
  ## Contributing
25
112
 
26
113
  ### Setup
@@ -44,8 +131,10 @@ $ bin/rails s
44
131
 
45
132
  ### Releasing
46
133
 
47
- 1. Bump the version in `lib/hotwire_combobox/version.rb`
48
- 2. Run `bundle exec rake release`
134
+ 1. Bump the version in `lib/hotwire_combobox/version.rb` (e.g. `VERSION = "0.1.0"`)
135
+ 2. Bump the version in `Gemfile.lock` (e.g. `hotwire_combobox (0.1.0)`)
136
+ 3. Commit the change (e.g. `git commit -am "Bump to 0.1.0"`)
137
+ 4. Run `bundle exec rake release`
49
138
 
50
139
  ## License
51
140
  The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
@@ -0,0 +1,99 @@
1
+ module HotwireCombobox
2
+ module Helper
3
+ def hwbox_option(...)
4
+ HotwireCombobox::Option.new(...)
5
+ end
6
+
7
+ def hwbox_options(options)
8
+ options.map { |option| hwbox_option(**option) }
9
+ end
10
+
11
+ def hw_combobox_tag(name, value = nil, form: nil, options: [], data: {}, input: {}, **attrs)
12
+ value_field_attrs = {}.tap do |h|
13
+ h[:id] = default_hw_combobox_value_field_id attrs, form, name
14
+ h[:name] = default_hw_combobox_value_field_name form, name
15
+ h[:data] = default_hw_combobox_value_field_data
16
+ h[:value] = form&.object&.public_send(name) || value
17
+ end
18
+
19
+ attrs[:type] ||= :text
20
+ attrs[:role] = :combobox
21
+ attrs[:id] = hw_combobox_id value_field_attrs[:id]
22
+ attrs[:data] = default_hw_combobox_data input.fetch(:data, {})
23
+ attrs[:aria] = default_hw_combobox_aria value_field_attrs, input.fetch(:aria, {})
24
+
25
+ render "hotwire_combobox/combobox", options: options,
26
+ attrs: attrs, value_field_attrs: value_field_attrs,
27
+ listbox_id: hw_combobox_listbox_id(value_field_attrs[:id]),
28
+ parent_data: default_hw_combobox_parent_data(attrs, data)
29
+ end
30
+
31
+ def hw_listbox_option_id(option)
32
+ option.try(:id)
33
+ end
34
+
35
+ def hw_listbox_option_value(option)
36
+ option.try(:value) || option.id
37
+ end
38
+
39
+ def hw_listbox_option_content(option)
40
+ option.try(:content) || option.try(:display)
41
+ end
42
+
43
+ def hw_listbox_option_filterable_as(option)
44
+ option.try(:filterable_as) || option.try(:display)
45
+ end
46
+
47
+ def hw_listbox_option_autocompletable_as(option)
48
+ option.try(:autocompletable_as) || option.try(:display)
49
+ end
50
+
51
+ private
52
+ def default_hw_combobox_value_field_id(attrs, form, name)
53
+ attrs.delete(:id) || form&.field_id(name)
54
+ end
55
+
56
+ def default_hw_combobox_value_field_name(form, name)
57
+ form&.field_name(name) || name
58
+ end
59
+
60
+ def default_hw_combobox_value_field_data
61
+ { "hw-combobox-target": "valueField" }
62
+ end
63
+
64
+ def default_hw_combobox_data(data)
65
+ data.reverse_merge! \
66
+ "action": "
67
+ focus->hw-combobox#open
68
+ input->hw-combobox#filter
69
+ keydown->hw-combobox#navigate
70
+ click@window->hw-combobox#closeOnClickOutside
71
+ focusin@window->hw-combobox#closeOnFocusOutside".squish,
72
+ "hw-combobox-target": "combobox"
73
+ end
74
+
75
+ def default_hw_combobox_aria(attrs, aria)
76
+ aria.reverse_merge! \
77
+ "controls": hw_combobox_listbox_id(attrs[:id]),
78
+ "owns": hw_combobox_listbox_id(attrs[:id]),
79
+ "haspopup": "listbox",
80
+ "autocomplete": "both"
81
+ end
82
+
83
+ def default_hw_combobox_parent_data(attrs, data)
84
+ data.reverse_merge! \
85
+ "controller": token_list("hw-combobox", data.delete(:controller)),
86
+ "hw-combobox-expanded-value": attrs.delete(:open),
87
+ "hw-combobox-filterable-attribute-value": "data-filterable-as",
88
+ "hw-combobox-autocompletable-attribute-value": "data-autocompletable-as"
89
+ end
90
+
91
+ def hw_combobox_id(id)
92
+ "#{id}-hw-combobox"
93
+ end
94
+
95
+ def hw_combobox_listbox_id(id)
96
+ "#{id}-hw-listbox"
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,21 @@
1
+ <%= tag.fieldset class: "hw-combobox", data: parent_data do %>
2
+ <%= hidden_field_tag value_field_attrs.delete(:name),
3
+ value_field_attrs.delete(:value), **value_field_attrs %>
4
+
5
+ <%= tag.input **attrs %>
6
+
7
+ <%= tag.ul id: listbox_id, hidden: "", role: :listbox,
8
+ data: { "hw-combobox-target": "listbox" } do |ul| %>
9
+ <% options.each do |option| %>
10
+ <%= tag.li hw_listbox_option_content(option),
11
+ id: hw_listbox_option_id(option),
12
+ role: :option,
13
+ style: "cursor: pointer;",
14
+ data: {
15
+ "action": "click->hw-combobox#selectOption",
16
+ "filterable-as": hw_listbox_option_filterable_as(option),
17
+ "autocompletable-as": hw_listbox_option_autocompletable_as(option),
18
+ "value": hw_listbox_option_value(option) } %>
19
+ <% end %>
20
+ <% end %>
21
+ <% end %>
@@ -0,0 +1,15 @@
1
+ module HotwireCombobox
2
+ class Engine < ::Rails::Engine
3
+ isolate_namespace HotwireCombobox
4
+
5
+ initializer "hotwire_combobox.view_helpers" do
6
+ ActiveSupport.on_load :action_controller do
7
+ helper HotwireCombobox::Helper
8
+ end
9
+ end
10
+
11
+ initializer "hotwire_combobox.importmap", before: "importmap" do |app|
12
+ app.config.importmap.paths << Engine.root.join("config/importmap.rb")
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,3 @@
1
+ module HotwireCombobox
2
+ VERSION = "0.1.7"
3
+ end
@@ -0,0 +1,6 @@
1
+ require "hotwire_combobox/version"
2
+ require "hotwire_combobox/engine"
3
+
4
+ module HotwireCombobox
5
+ Option = Struct.new(:id, :value, :display, :content, :filterable_as, :autocompletable_as)
6
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: hotwire_combobox
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.5
4
+ version: 0.1.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jose Farias
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-10-05 00:00:00.000000000 Z
11
+ date: 2023-12-10 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -62,16 +62,16 @@ files:
62
62
  - MIT-LICENSE
63
63
  - README.md
64
64
  - Rakefile
65
- - app/assets/javascripts/combobox_application.js
66
65
  - app/assets/javascripts/controllers/application.js
67
- - app/assets/javascripts/controllers/combobox_controller.js
66
+ - app/assets/javascripts/controllers/hw_combobox_controller.js
68
67
  - app/assets/javascripts/controllers/index.js
69
- - app/helpers/combobox/helper.rb
70
- - app/views/combobox/_combobox.html.erb
68
+ - app/assets/javascripts/hotwire_combobox_application.js
69
+ - app/helpers/hotwire_combobox/helper.rb
70
+ - app/views/hotwire_combobox/_combobox.html.erb
71
71
  - config/importmap.rb
72
- - lib/combobox.rb
73
- - lib/combobox/engine.rb
74
- - lib/combobox/version.rb
72
+ - lib/hotwire_combobox.rb
73
+ - lib/hotwire_combobox/engine.rb
74
+ - lib/hotwire_combobox/version.rb
75
75
  homepage: https://github.com/josefarias/hotwire_combobox
76
76
  licenses:
77
77
  - MIT
@@ -1,75 +0,0 @@
1
- module Combobox
2
- module Helper
3
- def combobox_tag(name, value = nil, form: nil, options: [], data: {}, input: {}, **attrs)
4
- value_field_attrs = {}.tap do |h|
5
- h[:id] = default_combobox_value_field_id attrs, form, name
6
- h[:name] = default_combobox_value_field_name form, name
7
- h[:data] = default_combobox_value_field_data
8
- h[:value] = form&.object&.public_send(name) || value
9
- end
10
-
11
- attrs[:type] ||= :text
12
- attrs[:role] = :combobox
13
- attrs[:id] = combobox_id(value_field_attrs[:id])
14
- attrs[:data] = default_combobox_data input.fetch(:data, {})
15
- attrs[:aria] = default_combobox_aria value_field_attrs, input.fetch(:aria, {})
16
-
17
- render "combobox/combobox", options: options,
18
- attrs: attrs, value_field_attrs: value_field_attrs,
19
- listbox_id: combobox_listbox_id(value_field_attrs[:id]),
20
- parent_data: default_combobox_parent_data(attrs, data)
21
- end
22
-
23
- def value_for_listbox_option(option)
24
- option.try(:value) || option.id
25
- end
26
-
27
- private
28
- def default_combobox_value_field_id(attrs, form, name)
29
- attrs.delete(:id) || form&.field_id(name)
30
- end
31
-
32
- def default_combobox_value_field_name(form, name)
33
- form&.field_name(name) || name
34
- end
35
-
36
- def default_combobox_value_field_data
37
- { "combobox-target": "valueField" }
38
- end
39
-
40
- def default_combobox_data(data)
41
- data.reverse_merge! \
42
- "action": "
43
- focus->combobox#open
44
- input->combobox#filter
45
- keydown->combobox#navigate
46
- click@window->combobox#closeOnClickOutside
47
- focusin@window->combobox#closeOnFocusOutside".squish,
48
- "combobox-target": "combobox"
49
- end
50
-
51
- def default_combobox_aria(attrs, aria)
52
- aria.reverse_merge! \
53
- "controls": combobox_listbox_id(attrs[:id]),
54
- "owns": combobox_listbox_id(attrs[:id]),
55
- "haspopup": "listbox",
56
- "autocomplete": "both"
57
- end
58
-
59
- def default_combobox_parent_data(attrs, data)
60
- data.reverse_merge! \
61
- "controller": token_list(:combobox, data.delete(:controller)),
62
- "combobox-expanded-value": attrs.delete(:open),
63
- "combobox-filterable-attribute-value": "data-filterable-as",
64
- "combobox-autocompletable-attribute-value": "data-autocompletable-as"
65
- end
66
-
67
- def combobox_id(id)
68
- "#{id}-combobox"
69
- end
70
-
71
- def combobox_listbox_id(id)
72
- "#{id}-listbox"
73
- end
74
- end
75
- end
@@ -1,19 +0,0 @@
1
- <%= tag.fieldset class: "hotwire-combobox", data: parent_data do %>
2
- <%= hidden_field_tag value_field_attrs.delete(:name),
3
- value_field_attrs.delete(:value), **value_field_attrs %>
4
-
5
- <%= tag.input **attrs %>
6
-
7
- <%= tag.ul id: listbox_id, hidden: "", role: :listbox,
8
- data: { "combobox-target": "listbox" } do |ul| %>
9
- <% options.each do |option| %>
10
- <%= tag.li option.content, id: option.try(:id),
11
- role: :option, style: "cursor: pointer;",
12
- data: {
13
- "action": "click->combobox#selectOption",
14
- "filterable-as": option.try(:filterable_as),
15
- "autocompletable-as": option.try(:autocompletable_as),
16
- "value": value_for_listbox_option(option) } %>
17
- <% end %>
18
- <% end %>
19
- <% end %>
@@ -1,15 +0,0 @@
1
- module Combobox
2
- class Engine < ::Rails::Engine
3
- isolate_namespace Combobox
4
-
5
- initializer "combobox.view_helpers" do
6
- ActiveSupport.on_load :action_controller do
7
- helper Combobox::Helper
8
- end
9
- end
10
-
11
- initializer "combobox.importmap", before: "importmap" do |app|
12
- app.config.importmap.paths << Engine.root.join("config/importmap.rb")
13
- end
14
- end
15
- end
@@ -1,3 +0,0 @@
1
- module Combobox
2
- VERSION = "0.1.5"
3
- end
data/lib/combobox.rb DELETED
@@ -1,6 +0,0 @@
1
- require "combobox/version"
2
- require "combobox/engine"
3
-
4
- module Combobox
5
- Option = Struct.new(:id, :value, :content, :filterable_as, :autocompletable_as)
6
- end