hotwire_combobox 0.1.5 → 0.1.7

Sign up to get free protection for your applications and to get access to all the features.
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