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