hotwire_combobox 0.1.9 → 0.1.11
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 +4 -4
- data/README.md +1 -4
- data/app/assets/javascripts/controllers/hw_combobox_controller.js +50 -14
- data/app/helpers/hotwire_combobox/helper.rb +29 -90
- data/app/presenters/hotwire_combobox/component.rb +127 -0
- data/app/presenters/hotwire_combobox/listbox/option.rb +50 -0
- data/app/views/hotwire_combobox/_combobox.html.erb +10 -16
- data/lib/hotwire_combobox/engine.rb +12 -2
- data/lib/hotwire_combobox/version.rb +1 -1
- metadata +4 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 0fabe58275ffd3010bca3cc350f94ebd673af65d7411156998418b7cd09c262e
|
4
|
+
data.tar.gz: 223c6a8e14566bc49cc900732ca234f559741b06f07c3e5d4724d9bf78b33732
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 4165ae08d560f369125e5542e26619fd2ccaea559232670d4dd1511cbf8972d7068b560ada69abb2ee61c0fad22826040bb9ced88a791192f0442e049a7c2124
|
7
|
+
data.tar.gz: 004c8565c5c9ba15950bda9e242315d2ad21f7ee3e0a28fff5c6fe6ee0b54fd495b9caed719df2ee5714cea096362e129be6339e19dd74ec82d9baf91da54672
|
data/README.md
CHANGED
@@ -59,9 +59,6 @@ The `options` argument takes an array of any objects which respond to:
|
|
59
59
|
| 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. |
|
60
60
|
| display | Used as a short-hand for other attributes. See the rest of the list for details. |
|
61
61
|
|
62
|
-
> [!NOTE]
|
63
|
-
> The `id` attribute is required only if `value` is not provided.
|
64
|
-
|
65
62
|
You can use the `combobox_options` helper to create an array of option objects which respond to the above methods:
|
66
63
|
|
67
64
|
```ruby
|
@@ -89,7 +86,7 @@ Additionally, you can pass the following [Stimulus class values](https://stimulu
|
|
89
86
|
|
90
87
|
### Validity
|
91
88
|
|
92
|
-
|
89
|
+
Unless `name_when_new` is passed, the hidden input can't have a value that's not in the list of options.
|
93
90
|
|
94
91
|
If a nonexistent value is typed into the combobox, the value of the hidden input will be set empty.
|
95
92
|
|
@@ -2,12 +2,17 @@ import { Controller } from "@hotwired/stimulus"
|
|
2
2
|
|
3
3
|
export default class extends Controller {
|
4
4
|
static classes = [ "selected", "invalid" ]
|
5
|
-
static targets = [ "combobox", "listbox", "
|
6
|
-
static values = {
|
5
|
+
static targets = [ "combobox", "listbox", "hiddenField" ]
|
6
|
+
static values = {
|
7
|
+
expanded: Boolean,
|
8
|
+
nameWhenNew: String,
|
9
|
+
originalName: String,
|
10
|
+
filterableAttribute: String,
|
11
|
+
autocompletableAttribute: String }
|
7
12
|
|
8
13
|
connect() {
|
9
|
-
if (this.
|
10
|
-
this.selectOptionByValue(this.
|
14
|
+
if (this.hiddenFieldTarget.value) {
|
15
|
+
this.selectOptionByValue(this.hiddenFieldTarget.value)
|
11
16
|
}
|
12
17
|
}
|
13
18
|
|
@@ -16,6 +21,7 @@ export default class extends Controller {
|
|
16
21
|
}
|
17
22
|
|
18
23
|
close() {
|
24
|
+
if (!this.isOpen) return
|
19
25
|
this.commitSelection()
|
20
26
|
this.expandedValue = false
|
21
27
|
}
|
@@ -26,13 +32,16 @@ export default class extends Controller {
|
|
26
32
|
}
|
27
33
|
|
28
34
|
filter(event) {
|
35
|
+
const isDeleting = event.inputType === "deleteContentBackward"
|
29
36
|
const query = this.comboboxTarget.value.trim()
|
30
37
|
|
31
38
|
this.open()
|
32
39
|
|
33
40
|
this.allOptionElements.forEach(applyFilter(query, { matching: this.filterableAttributeValue }))
|
34
41
|
|
35
|
-
if (
|
42
|
+
if (this.isValidNewOption(query, { ignoreAutocomplete: isDeleting })) {
|
43
|
+
this.selectNew(query)
|
44
|
+
} else if (isDeleting) {
|
36
45
|
this.deselect(this.selectedOptionElement)
|
37
46
|
} else {
|
38
47
|
this.select(this.visibleOptionElements[0])
|
@@ -47,7 +56,6 @@ export default class extends Controller {
|
|
47
56
|
if (this.element.contains(target)) return
|
48
57
|
|
49
58
|
this.close()
|
50
|
-
target.focus()
|
51
59
|
}
|
52
60
|
|
53
61
|
closeOnFocusOutside({ target }) {
|
@@ -56,7 +64,6 @@ export default class extends Controller {
|
|
56
64
|
if (target.matches("main")) return
|
57
65
|
|
58
66
|
this.close()
|
59
|
-
target.focus()
|
60
67
|
}
|
61
68
|
|
62
69
|
// private
|
@@ -85,7 +92,9 @@ export default class extends Controller {
|
|
85
92
|
}
|
86
93
|
|
87
94
|
commitSelection() {
|
88
|
-
this.
|
95
|
+
if (!this.isValidNewOption(this.comboboxTarget.value, { ignoreAutocomplete: true })) {
|
96
|
+
this.select(this.selectedOptionElement, { force: true })
|
97
|
+
}
|
89
98
|
}
|
90
99
|
|
91
100
|
expandedValueChanged() {
|
@@ -107,7 +116,7 @@ export default class extends Controller {
|
|
107
116
|
}
|
108
117
|
|
109
118
|
select(option, { force = false } = {}) {
|
110
|
-
this.
|
119
|
+
this.resetOptions()
|
111
120
|
|
112
121
|
if (option) {
|
113
122
|
if (this.hasSelectedClass) option.classList.add(this.selectedClass)
|
@@ -125,6 +134,17 @@ export default class extends Controller {
|
|
125
134
|
}
|
126
135
|
}
|
127
136
|
|
137
|
+
selectNew(query) {
|
138
|
+
this.resetOptions()
|
139
|
+
this.hiddenFieldTarget.value = query
|
140
|
+
this.hiddenFieldTarget.name = this.nameWhenNewValue
|
141
|
+
}
|
142
|
+
|
143
|
+
resetOptions() {
|
144
|
+
this.allOptionElements.forEach(option => this.deselect(option))
|
145
|
+
this.hiddenFieldTarget.name = this.originalNameValue
|
146
|
+
}
|
147
|
+
|
128
148
|
selectIndex(index) {
|
129
149
|
const option = wrapAroundAccess(this.visibleOptionElements, index)
|
130
150
|
this.select(option, { force: true })
|
@@ -144,26 +164,34 @@ export default class extends Controller {
|
|
144
164
|
executeSelect(option, { selected }) {
|
145
165
|
if (selected) {
|
146
166
|
option.setAttribute("aria-selected", true)
|
147
|
-
this.
|
167
|
+
this.hiddenFieldTarget.value = option.dataset.value
|
148
168
|
} else {
|
149
169
|
option.setAttribute("aria-selected", false)
|
150
|
-
this.
|
170
|
+
this.hiddenFieldTarget.value = null
|
151
171
|
}
|
152
172
|
}
|
153
173
|
|
154
174
|
maybeAutocompleteWith(option, { force }) {
|
155
175
|
const typedValue = this.comboboxTarget.value
|
156
|
-
const autocompletedValue = option.
|
176
|
+
const autocompletedValue = option.getAttribute(this.autocompletableAttributeValue)
|
157
177
|
|
158
178
|
if (force) {
|
159
179
|
this.comboboxTarget.value = autocompletedValue
|
160
180
|
this.comboboxTarget.setSelectionRange(autocompletedValue.length, autocompletedValue.length)
|
161
|
-
} else if (
|
181
|
+
} else if (startsWith(autocompletedValue, typedValue)) {
|
162
182
|
this.comboboxTarget.value = autocompletedValue
|
163
183
|
this.comboboxTarget.setSelectionRange(typedValue.length, autocompletedValue.length)
|
164
184
|
}
|
165
185
|
}
|
166
186
|
|
187
|
+
isValidNewOption(query, { ignoreAutocomplete = false } = {}) {
|
188
|
+
const typedValue = this.comboboxTarget.value
|
189
|
+
const autocompletedValue = this.visibleOptionElements[0]?.getAttribute(this.autocompletableAttributeValue)
|
190
|
+
const insufficentAutocomplete = !autocompletedValue || !startsWith(autocompletedValue, typedValue)
|
191
|
+
|
192
|
+
return query.length > 0 && this.allowNew && (ignoreAutocomplete || insufficentAutocomplete)
|
193
|
+
}
|
194
|
+
|
167
195
|
get allOptions() {
|
168
196
|
return Array.from(this.allOptionElements)
|
169
197
|
}
|
@@ -189,9 +217,13 @@ export default class extends Controller {
|
|
189
217
|
}
|
190
218
|
|
191
219
|
get valueIsInvalid() {
|
192
|
-
const isRequiredAndEmpty = this.comboboxTarget.required && !this.
|
220
|
+
const isRequiredAndEmpty = this.comboboxTarget.required && !this.hiddenFieldTarget.value
|
193
221
|
return isRequiredAndEmpty
|
194
222
|
}
|
223
|
+
|
224
|
+
get allowNew() {
|
225
|
+
return !!this.nameWhenNewValue
|
226
|
+
}
|
195
227
|
}
|
196
228
|
|
197
229
|
function applyFilter(query, { matching }) {
|
@@ -224,3 +256,7 @@ function cancel(event) {
|
|
224
256
|
event.stopPropagation()
|
225
257
|
event.preventDefault()
|
226
258
|
}
|
259
|
+
|
260
|
+
function startsWith(string, substring) {
|
261
|
+
return string.toLowerCase().startsWith(substring.toLowerCase())
|
262
|
+
}
|
@@ -1,105 +1,44 @@
|
|
1
1
|
module HotwireCombobox
|
2
2
|
module Helper
|
3
|
+
NonCollectionOptions = Class.new StandardError
|
4
|
+
|
3
5
|
class << self
|
4
6
|
delegate :bypass_convenience_methods?, to: :HotwireCombobox
|
5
|
-
end
|
6
7
|
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
8
|
+
def hw_combobox_options(options, **methods)
|
9
|
+
unless options.respond_to?(:map)
|
10
|
+
raise NonCollectionOptions, "options must be an Array or an ActiveRecord::Relation"
|
11
|
+
end
|
11
12
|
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
attrs[:type] ||= :text
|
21
|
-
attrs[:role] = :combobox
|
22
|
-
attrs[:id] = hw_combobox_id value_field_attrs[:id]
|
23
|
-
attrs[:data] = default_hw_combobox_data input.fetch(:data, {})
|
24
|
-
attrs[:aria] = default_hw_combobox_aria value_field_attrs, input.fetch(:aria, {})
|
25
|
-
|
26
|
-
render "hotwire_combobox/combobox", options: options,
|
27
|
-
attrs: attrs, value_field_attrs: value_field_attrs,
|
28
|
-
listbox_id: hw_combobox_listbox_id(value_field_attrs[:id]),
|
29
|
-
parent_data: default_hw_combobox_parent_data(attrs, data)
|
30
|
-
end
|
31
|
-
alias_method :combobox_tag, :hw_combobox_tag unless bypass_convenience_methods?
|
13
|
+
if ActiveRecord::Relation === options || ActiveRecord::Base === options.first
|
14
|
+
options.map do |option|
|
15
|
+
attrs = {}.tap do |attrs|
|
16
|
+
attrs[:id] = option.public_send(methods[:id]) if methods[:id]
|
17
|
+
attrs[:value] = option.public_send(methods[:value] || :id)
|
18
|
+
attrs[:display] = option.public_send(methods[:display]) if methods[:display]
|
19
|
+
end
|
32
20
|
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
end
|
21
|
+
hw_combobox_option(**attrs)
|
22
|
+
end
|
23
|
+
else
|
24
|
+
options.map { |option| hw_combobox_option(**option) }
|
25
|
+
end
|
26
|
+
end
|
40
27
|
|
41
|
-
|
42
|
-
|
28
|
+
private
|
29
|
+
def hw_combobox_option(*args, **kwargs, &block)
|
30
|
+
HotwireCombobox::Option.new(*args, **kwargs, &block)
|
31
|
+
end
|
43
32
|
end
|
44
33
|
|
45
|
-
def
|
46
|
-
|
34
|
+
def hw_combobox_tag(*args, **kwargs, &block)
|
35
|
+
render "hotwire_combobox/combobox", component: HotwireCombobox::Component.new(*args, **kwargs, &block)
|
47
36
|
end
|
37
|
+
alias_method :combobox_tag, :hw_combobox_tag unless bypass_convenience_methods?
|
48
38
|
|
49
|
-
def
|
50
|
-
|
39
|
+
def hw_combobox_options(*args, **kwargs, &block)
|
40
|
+
HotwireCombobox::Helper.hw_combobox_options(*args, **kwargs, &block)
|
51
41
|
end
|
52
|
-
|
53
|
-
private
|
54
|
-
def hw_combobox_option(...)
|
55
|
-
HotwireCombobox::Option.new(...)
|
56
|
-
end
|
57
|
-
|
58
|
-
def default_hw_combobox_value_field_id(attrs, form, name)
|
59
|
-
attrs.delete(:id) || form&.field_id(name)
|
60
|
-
end
|
61
|
-
|
62
|
-
def default_hw_combobox_value_field_name(form, name)
|
63
|
-
form&.field_name(name) || name
|
64
|
-
end
|
65
|
-
|
66
|
-
def default_hw_combobox_value_field_data
|
67
|
-
{ "hw-combobox-target": "valueField" }
|
68
|
-
end
|
69
|
-
|
70
|
-
def default_hw_combobox_data(data)
|
71
|
-
data.reverse_merge! \
|
72
|
-
"action": "
|
73
|
-
focus->hw-combobox#open
|
74
|
-
input->hw-combobox#filter
|
75
|
-
keydown->hw-combobox#navigate
|
76
|
-
click@window->hw-combobox#closeOnClickOutside
|
77
|
-
focusin@window->hw-combobox#closeOnFocusOutside".squish,
|
78
|
-
"hw-combobox-target": "combobox"
|
79
|
-
end
|
80
|
-
|
81
|
-
def default_hw_combobox_aria(attrs, aria)
|
82
|
-
aria.reverse_merge! \
|
83
|
-
"controls": hw_combobox_listbox_id(attrs[:id]),
|
84
|
-
"owns": hw_combobox_listbox_id(attrs[:id]),
|
85
|
-
"haspopup": "listbox",
|
86
|
-
"autocomplete": "both"
|
87
|
-
end
|
88
|
-
|
89
|
-
def default_hw_combobox_parent_data(attrs, data)
|
90
|
-
data.reverse_merge! \
|
91
|
-
"controller": token_list("hw-combobox", data.delete(:controller)),
|
92
|
-
"hw-combobox-expanded-value": attrs.delete(:open),
|
93
|
-
"hw-combobox-filterable-attribute-value": "data-filterable-as",
|
94
|
-
"hw-combobox-autocompletable-attribute-value": "data-autocompletable-as"
|
95
|
-
end
|
96
|
-
|
97
|
-
def hw_combobox_id(id)
|
98
|
-
"#{id}-hw-combobox"
|
99
|
-
end
|
100
|
-
|
101
|
-
def hw_combobox_listbox_id(id)
|
102
|
-
"#{id}-hw-listbox"
|
103
|
-
end
|
42
|
+
alias_method :combobox_options, :hw_combobox_options unless bypass_convenience_methods?
|
104
43
|
end
|
105
44
|
end
|
@@ -0,0 +1,127 @@
|
|
1
|
+
class HotwireCombobox::Component
|
2
|
+
include ActionView::Helpers::TagHelper
|
3
|
+
|
4
|
+
class << self
|
5
|
+
def options_from(options)
|
6
|
+
if HotwireCombobox::Option === options.first
|
7
|
+
options
|
8
|
+
else
|
9
|
+
HotwireCombobox::Helper.hw_combobox_options options, display: :to_combobox_display
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def initialize(name, value = nil, id: nil, form: nil, name_when_new: nil, open: false, options: [], data: {}, input: {}, **rest)
|
15
|
+
@combobox_attrs = input.reverse_merge(rest).with_indifferent_access # input: {} allows for specifying e.g. data attributes on the input field
|
16
|
+
@options = self.class.options_from options
|
17
|
+
@id, @name, @value, @form, @name_when_new, @open, @data = id, name, value, form, name_when_new, open, data
|
18
|
+
end
|
19
|
+
|
20
|
+
def fieldset_attrs
|
21
|
+
{
|
22
|
+
class: "hw-combobox",
|
23
|
+
data: fieldset_data
|
24
|
+
}
|
25
|
+
end
|
26
|
+
|
27
|
+
def hidden_field_attrs
|
28
|
+
{
|
29
|
+
id: hidden_field_id,
|
30
|
+
name: hidden_field_name,
|
31
|
+
data: hidden_field_data,
|
32
|
+
value: hidden_field_value
|
33
|
+
}
|
34
|
+
end
|
35
|
+
|
36
|
+
def input_attrs
|
37
|
+
nested_attrs = %i[ data aria ]
|
38
|
+
|
39
|
+
{
|
40
|
+
id: input_id,
|
41
|
+
role: :combobox,
|
42
|
+
type: input_type,
|
43
|
+
data: input_data,
|
44
|
+
aria: input_aria
|
45
|
+
}.merge combobox_attrs.except(*nested_attrs)
|
46
|
+
end
|
47
|
+
|
48
|
+
def listbox_attrs
|
49
|
+
{
|
50
|
+
id: listbox_id,
|
51
|
+
role: :listbox,
|
52
|
+
hidden: "",
|
53
|
+
data: listbox_data
|
54
|
+
}
|
55
|
+
end
|
56
|
+
|
57
|
+
def listbox_options
|
58
|
+
options.map { |option| HotwireCombobox::Listbox::Option.new option }
|
59
|
+
end
|
60
|
+
|
61
|
+
private
|
62
|
+
attr_reader :id, :name, :value, :form, :name_when_new, :open, :options, :data, :combobox_attrs
|
63
|
+
|
64
|
+
def fieldset_data
|
65
|
+
data.reverse_merge \
|
66
|
+
controller: token_list("hw-combobox", data[:controller]),
|
67
|
+
hw_combobox_expanded_value: open,
|
68
|
+
hw_combobox_name_when_new_value: name_when_new,
|
69
|
+
hw_combobox_original_name_value: hidden_field_name,
|
70
|
+
hw_combobox_filterable_attribute_value: "data-filterable-as",
|
71
|
+
hw_combobox_autocompletable_attribute_value: "data-autocompletable-as"
|
72
|
+
end
|
73
|
+
|
74
|
+
|
75
|
+
def hidden_field_id
|
76
|
+
id || form&.field_id(name)
|
77
|
+
end
|
78
|
+
|
79
|
+
def hidden_field_name
|
80
|
+
form&.field_name(name) || name
|
81
|
+
end
|
82
|
+
|
83
|
+
def hidden_field_data
|
84
|
+
{ hw_combobox_target: "hiddenField" }
|
85
|
+
end
|
86
|
+
|
87
|
+
def hidden_field_value
|
88
|
+
form&.object&.public_send(name) || value
|
89
|
+
end
|
90
|
+
|
91
|
+
|
92
|
+
def input_id
|
93
|
+
"#{hidden_field_id}-hw-combobox"
|
94
|
+
end
|
95
|
+
|
96
|
+
def input_type
|
97
|
+
combobox_attrs[:type].to_s.presence_in(%w[ text search ]) || "text"
|
98
|
+
end
|
99
|
+
|
100
|
+
def input_data
|
101
|
+
combobox_attrs.fetch(:data, {}).reverse_merge! \
|
102
|
+
action: "
|
103
|
+
focus->hw-combobox#open
|
104
|
+
input->hw-combobox#filter
|
105
|
+
keydown->hw-combobox#navigate
|
106
|
+
click@window->hw-combobox#closeOnClickOutside
|
107
|
+
focusin@window->hw-combobox#closeOnFocusOutside".squish,
|
108
|
+
hw_combobox_target: "combobox"
|
109
|
+
end
|
110
|
+
|
111
|
+
def input_aria
|
112
|
+
combobox_attrs.fetch(:aria, {}).reverse_merge! \
|
113
|
+
controls: listbox_id,
|
114
|
+
owns: listbox_id,
|
115
|
+
haspopup: "listbox",
|
116
|
+
autocomplete: "both"
|
117
|
+
end
|
118
|
+
|
119
|
+
|
120
|
+
def listbox_id
|
121
|
+
"#{hidden_field_id}-hw-listbox"
|
122
|
+
end
|
123
|
+
|
124
|
+
def listbox_data
|
125
|
+
{ hw_combobox_target: "listbox" }
|
126
|
+
end
|
127
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
class HotwireCombobox::Listbox::Option
|
2
|
+
def initialize(option)
|
3
|
+
@option = option
|
4
|
+
end
|
5
|
+
|
6
|
+
def render_in(view_context)
|
7
|
+
view_context.tag.li content, **options
|
8
|
+
end
|
9
|
+
|
10
|
+
private
|
11
|
+
attr_reader :option
|
12
|
+
|
13
|
+
def content
|
14
|
+
option.try(:content) || option.try(:display)
|
15
|
+
end
|
16
|
+
|
17
|
+
def options
|
18
|
+
{
|
19
|
+
id: id,
|
20
|
+
role: :option,
|
21
|
+
style: "cursor: pointer;",
|
22
|
+
data: data
|
23
|
+
}
|
24
|
+
end
|
25
|
+
|
26
|
+
def id
|
27
|
+
option.try(:id)
|
28
|
+
end
|
29
|
+
|
30
|
+
def data
|
31
|
+
{
|
32
|
+
action: "click->hw-combobox#selectOption",
|
33
|
+
filterable_as: filterable_as,
|
34
|
+
autocompletable_as: autocompletable_as,
|
35
|
+
value: value
|
36
|
+
}
|
37
|
+
end
|
38
|
+
|
39
|
+
def filterable_as
|
40
|
+
option.try(:filterable_as) || option.try(:display)
|
41
|
+
end
|
42
|
+
|
43
|
+
def autocompletable_as
|
44
|
+
option.try(:autocompletable_as) || option.try(:display)
|
45
|
+
end
|
46
|
+
|
47
|
+
def value
|
48
|
+
option.try(:value) || option.id
|
49
|
+
end
|
50
|
+
end
|
@@ -1,21 +1,15 @@
|
|
1
|
-
<%= tag.fieldset
|
2
|
-
|
3
|
-
|
1
|
+
<%= tag.fieldset **component.fieldset_attrs do %>
|
2
|
+
<% hidden_field_attrs = component.hidden_field_attrs %>
|
3
|
+
<%= hidden_field_tag \
|
4
|
+
hidden_field_attrs.delete(:name),
|
5
|
+
hidden_field_attrs.delete(:value),
|
6
|
+
**hidden_field_attrs %>
|
4
7
|
|
5
|
-
<%= tag.input **
|
8
|
+
<%= tag.input **component.input_attrs %>
|
6
9
|
|
7
|
-
<%= tag.ul
|
8
|
-
|
9
|
-
|
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) } %>
|
10
|
+
<%= tag.ul **component.listbox_attrs do |ul| %>
|
11
|
+
<% component.listbox_options.each do |option| %>
|
12
|
+
<%= render option %>
|
19
13
|
<% end %>
|
20
14
|
<% end %>
|
21
15
|
<% end %>
|
@@ -3,8 +3,18 @@ module HotwireCombobox
|
|
3
3
|
isolate_namespace HotwireCombobox
|
4
4
|
|
5
5
|
initializer "hotwire_combobox.view_helpers" do
|
6
|
-
ActiveSupport.on_load :
|
7
|
-
|
6
|
+
ActiveSupport.on_load :action_view do
|
7
|
+
include HotwireCombobox::Helper
|
8
|
+
|
9
|
+
unless HotwireCombobox.bypass_convenience_methods?
|
10
|
+
module FormBuilderExtensions
|
11
|
+
def combobox(*args, **kwargs, &block)
|
12
|
+
@template.hw_combobox_tag *args, **kwargs.merge(form: self), &block
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
ActionView::Helpers::FormBuilder.include FormBuilderExtensions
|
17
|
+
end
|
8
18
|
end
|
9
19
|
end
|
10
20
|
|
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.
|
4
|
+
version: 0.1.11
|
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-12-
|
11
|
+
date: 2023-12-30 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rails
|
@@ -67,6 +67,8 @@ files:
|
|
67
67
|
- app/assets/javascripts/controllers/index.js
|
68
68
|
- app/assets/javascripts/hotwire_combobox_application.js
|
69
69
|
- app/helpers/hotwire_combobox/helper.rb
|
70
|
+
- app/presenters/hotwire_combobox/component.rb
|
71
|
+
- app/presenters/hotwire_combobox/listbox/option.rb
|
70
72
|
- app/views/hotwire_combobox/_combobox.html.erb
|
71
73
|
- config/importmap.rb
|
72
74
|
- lib/hotwire_combobox.rb
|