hotwire_combobox 0.1.13 → 0.1.15
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +10 -104
- data/app/assets/javascripts/controllers/hw_combobox_controller.js +7 -1
- data/app/assets/javascripts/models/combobox/dialog.js +7 -5
- data/app/assets/javascripts/models/combobox/options.js +2 -2
- data/app/assets/javascripts/models/combobox/selection.js +26 -12
- data/app/assets/javascripts/models/combobox/toggle.js +22 -2
- data/app/helpers/hotwire_combobox/helper.rb +22 -11
- data/app/presenters/hotwire_combobox/component.rb +50 -16
- data/app/presenters/hotwire_combobox/listbox/option.rb +10 -10
- data/app/views/hotwire_combobox/_next_page.turbo_stream.erb +3 -3
- data/app/views/hotwire_combobox/_pagination.html.erb +3 -2
- data/lib/hotwire_combobox/engine.rb +2 -2
- data/lib/hotwire_combobox/version.rb +1 -1
- metadata +4 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: aa358f06557f2f70b072650e12b11a270fc3afa36addc8e73820105ea7383da5
|
4
|
+
data.tar.gz: e35a86d210a02af308614ebdb983a3af69026824ea327fc0f1e353a9612bf807
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 011a0b91f679374998444836f121bdede29a86fc8d4c905dd5f0988e41b834be84044fd9cd9a1cc91e88d3bd7812f16b727d9144764968fe07bb44b6d8e06a66
|
7
|
+
data.tar.gz: 7ff6b81587de0d53496fcb008cffa0c9ff4df8d19d1f9e8583b7becba82234252b60a57cf4cc258fb5b633a9e5d90821991a14bcb341732a59454a8b50b996d2
|
data/README.md
CHANGED
@@ -1,115 +1,21 @@
|
|
1
|
-
|
1
|
+
<p align="center">
|
2
|
+
<img src="docs/assets/images/logo.png" height=150>
|
3
|
+
</p>
|
2
4
|
|
3
|
-
|
5
|
+
# Autocomplete for Rails apps using Hotwire
|
4
6
|
|
5
|
-
|
6
|
-
> This gem is pre-release software. It's not ready for production use yet and the API is subject to change.
|
7
|
+
[![CI Tests](https://github.com/josefarias/hotwire_combobox/actions/workflows/ci_tests.yml/badge.svg)](https://github.com/josefarias/hotwire_combobox/actions/workflows/ci_tests.yml) [![Gem Version](https://badge.fury.io/rb/hotwire_combobox.svg)](https://badge.fury.io/rb/hotwire_combobox)
|
7
8
|
|
8
|
-
## Installation
|
9
|
-
|
10
|
-
Add this line to your application's Gemfile:
|
11
|
-
|
12
|
-
```ruby
|
13
|
-
gem "hotwire_combobox"
|
14
|
-
```
|
15
|
-
|
16
|
-
And then execute:
|
17
|
-
```bash
|
18
|
-
$ bundle
|
19
|
-
```
|
20
|
-
|
21
|
-
## Output
|
22
|
-
|
23
|
-
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.
|
24
|
-
|
25
|
-
```html
|
26
|
-
<fieldset class="hw-combobox">
|
27
|
-
<input type="hidden" name="provided_name">
|
28
|
-
|
29
|
-
<input type="text" role="combobox">
|
30
|
-
|
31
|
-
<ul role="listbox">
|
32
|
-
<li role="option">Provided Option 1 Content</li>
|
33
|
-
<li role="option">Provided Option 2 Content</li>
|
34
|
-
<!-- ... -->
|
35
|
-
</ul>
|
36
|
-
</fieldset>
|
37
|
-
```
|
38
|
-
|
39
|
-
The `<ul role="listbox">` element is what gets shown when the combobox is open.
|
40
|
-
|
41
|
-
The library uses stimulus to add interactivity to the combobox and sync the input element's value with the hidden input element.
|
42
|
-
|
43
|
-
The hidden input's value is what ultimately gets sent to the server when submitting a form containing the combobox.
|
44
|
-
|
45
|
-
## Usage
|
46
|
-
|
47
|
-
### Options
|
48
|
-
|
49
|
-
Options are what you see when you open the combobox.
|
50
9
|
|
51
|
-
|
10
|
+
> [!IMPORTANT]
|
11
|
+
> We need your help to finalize this gem's first major release. Please use it in your apps and report any issues.
|
52
12
|
|
53
|
-
|
54
|
-
|--------------------|--------------------------------------------------------------------------------------------------------------------------------------------|
|
55
|
-
| id | Used as the option element's `id` attribute. Only required if `value` is not provided. |
|
56
|
-
| value | Used to populate the input element's `value` attribute. Falls back to calling `id` on the object if not provided. |
|
57
|
-
| content | **Supports HTML** <br> Used as the option element's content. Falls back to calling `display` on the object if not provided. |
|
58
|
-
| 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. |
|
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
|
-
| display | Used as a short-hand for other attributes. See the rest of the list for details. |
|
61
|
-
|
62
|
-
You can use the `combobox_options` helper to create an array of option objects which respond to the above methods:
|
63
|
-
|
64
|
-
```ruby
|
65
|
-
combobox_options [
|
66
|
-
{ value: "AL", display: "Alabama" },
|
67
|
-
{ value: "AK", display: "Alaska" },
|
68
|
-
{ value: "AZ", display: "Arizona" },
|
69
|
-
# ...
|
70
|
-
]
|
71
|
-
```
|
72
|
-
|
73
|
-
### Styling
|
74
|
-
|
75
|
-
The combobox is completely unstyled by default. You can use the following CSS selectors to style the combobox:
|
76
|
-
|
77
|
-
* `.hw-combobox` targets the `<fieldset>` element used to wrap the whole component.
|
78
|
-
* `.hw-combobox [role="combobox"]` targets the input element.
|
79
|
-
* `.hw-combobox [role="listbox"]` targets the listbox which encloses all option elements.
|
80
|
-
* `.hw-combobox [role="option"]` targets each option element inside the listbox.
|
81
|
-
|
82
|
-
Additionally, you can pass the following [Stimulus class values](https://stimulus.hotwired.dev/reference/css-classes) to `combobox_tag`:
|
83
|
-
|
84
|
-
* `data-hw-combobox-selected-class`: The class to apply to the selected option while shown inside an open listbox.
|
85
|
-
* `data-hw-combobox-invalid-class`: The class to apply to the input element when the current value is invalid.
|
86
|
-
|
87
|
-
### Validity
|
88
|
-
|
89
|
-
Unless `name_when_new` is passed, the hidden input can't have a value that's not in the list of options.
|
90
|
-
|
91
|
-
If a nonexistent value is typed into the combobox, the value of the hidden input will be set empty.
|
92
|
-
|
93
|
-
The only way a value can be marked as invalid is if the field is required and empty after having interacted with the combobox.
|
94
|
-
|
95
|
-
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.
|
96
|
-
|
97
|
-
> [!CAUTION]
|
98
|
-
> Bad actors can still submit invalid values to the server. You should always validate the input on the server side.
|
99
|
-
|
100
|
-
### Naming Conflicts
|
101
|
-
|
102
|
-
If your application has naming conflicts with this gem, the following config will turn:
|
13
|
+
## Installation
|
103
14
|
|
104
|
-
|
105
|
-
* `#combobox_options` into `#hw_combobox_options`
|
15
|
+
Add this line to your application's Gemfile and run `bundle install`:
|
106
16
|
|
107
17
|
```ruby
|
108
|
-
|
109
|
-
|
110
|
-
HotwireCombobox.setup do |config|
|
111
|
-
config.bypass_convenience_methods = true
|
112
|
-
end
|
18
|
+
gem "hotwire_combobox"
|
113
19
|
```
|
114
20
|
|
115
21
|
## Contributing
|
@@ -30,7 +30,8 @@ export default class HwComboboxController extends Concerns(...concerns) {
|
|
30
30
|
"dialogListbox",
|
31
31
|
"handle",
|
32
32
|
"hiddenField",
|
33
|
-
"listbox"
|
33
|
+
"listbox",
|
34
|
+
"paginationFrame"
|
34
35
|
]
|
35
36
|
|
36
37
|
static values = {
|
@@ -41,6 +42,7 @@ export default class HwComboboxController extends Concerns(...concerns) {
|
|
41
42
|
filterableAttribute: String,
|
42
43
|
nameWhenNew: String,
|
43
44
|
originalName: String,
|
45
|
+
prefilledDisplay: String,
|
44
46
|
smallViewportMaxWidth: String
|
45
47
|
}
|
46
48
|
|
@@ -66,4 +68,8 @@ export default class HwComboboxController extends Concerns(...concerns) {
|
|
66
68
|
this._collapse()
|
67
69
|
}
|
68
70
|
}
|
71
|
+
|
72
|
+
paginationFrameTargetConnected() {
|
73
|
+
this._preselectOption()
|
74
|
+
}
|
69
75
|
}
|
@@ -14,7 +14,7 @@ Combobox.Dialog = Base => class extends Base {
|
|
14
14
|
}
|
15
15
|
|
16
16
|
_moveArtifactsToDialog() {
|
17
|
-
this.dialogComboboxTarget.value = this.
|
17
|
+
this.dialogComboboxTarget.value = this._actingCombobox.value
|
18
18
|
|
19
19
|
this._actingCombobox = this.dialogComboboxTarget
|
20
20
|
this._actingListbox = this.dialogListboxTarget
|
@@ -23,7 +23,7 @@ Combobox.Dialog = Base => class extends Base {
|
|
23
23
|
}
|
24
24
|
|
25
25
|
_moveArtifactsInline() {
|
26
|
-
this.comboboxTarget.value = this.
|
26
|
+
this.comboboxTarget.value = this._actingCombobox.value
|
27
27
|
|
28
28
|
this._actingCombobox = this.comboboxTarget
|
29
29
|
this._actingListbox = this.listboxTarget
|
@@ -32,9 +32,11 @@ Combobox.Dialog = Base => class extends Base {
|
|
32
32
|
}
|
33
33
|
|
34
34
|
_resizeDialog = () => {
|
35
|
-
|
36
|
-
|
37
|
-
|
35
|
+
if (window.visualViewport) {
|
36
|
+
const fullHeight = window.innerHeight
|
37
|
+
const viewportHeight = window.visualViewport.height
|
38
|
+
this.dialogTarget.style.setProperty("--hw-dialog-bottom-padding", `${fullHeight - viewportHeight}px`)
|
39
|
+
}
|
38
40
|
}
|
39
41
|
|
40
42
|
// After closing a dialog, focus returns to the last focused element.
|
@@ -10,9 +10,9 @@ Combobox.Options = Base => class extends Base {
|
|
10
10
|
_isValidNewOption(query, { ignoreAutocomplete = false } = {}) {
|
11
11
|
const typedValue = this._actingCombobox.value
|
12
12
|
const autocompletedValue = this._visibleOptionElements[0]?.getAttribute(this.autocompletableAttributeValue)
|
13
|
-
const
|
13
|
+
const insufficientAutocomplete = !autocompletedValue || !startsWith(autocompletedValue, typedValue)
|
14
14
|
|
15
|
-
return query.length > 0 && this._allowNew && (ignoreAutocomplete ||
|
15
|
+
return query.length > 0 && this._allowNew && (ignoreAutocomplete || insufficientAutocomplete)
|
16
16
|
}
|
17
17
|
|
18
18
|
get _allowNew() {
|
@@ -8,8 +8,8 @@ Combobox.Selection = Base => class extends Base {
|
|
8
8
|
}
|
9
9
|
|
10
10
|
_connectSelection() {
|
11
|
-
if (this.
|
12
|
-
this.
|
11
|
+
if (this.hasPrefilledDisplayValue) {
|
12
|
+
this._actingCombobox.value = this.prefilledDisplayValue
|
13
13
|
}
|
14
14
|
}
|
15
15
|
|
@@ -17,8 +17,6 @@ Combobox.Selection = Base => class extends Base {
|
|
17
17
|
this._resetOptions()
|
18
18
|
|
19
19
|
if (option) {
|
20
|
-
if (this.hasSelectedClass) option.classList.add(this.selectedClass)
|
21
|
-
|
22
20
|
this._markValid()
|
23
21
|
this._maybeAutocompleteWith(option, { force })
|
24
22
|
this._commitSelection(option, { selected: true })
|
@@ -28,21 +26,27 @@ Combobox.Selection = Base => class extends Base {
|
|
28
26
|
}
|
29
27
|
|
30
28
|
_commitSelection(option, { selected }) {
|
31
|
-
option
|
32
|
-
option?.scrollIntoView({ block: "nearest" })
|
29
|
+
this._markSelected(option, { selected })
|
33
30
|
|
34
31
|
if (selected) {
|
35
|
-
this.hiddenFieldTarget.value = option
|
32
|
+
this.hiddenFieldTarget.value = option.dataset.value
|
33
|
+
option.scrollIntoView({ block: "nearest" })
|
36
34
|
} else {
|
37
35
|
this.hiddenFieldTarget.value = null
|
38
36
|
}
|
39
37
|
}
|
40
38
|
|
39
|
+
_markSelected(option, { selected }) {
|
40
|
+
if (this.hasSelectedClass) {
|
41
|
+
option.classList.toggle(this.selectedClass, selected)
|
42
|
+
}
|
43
|
+
|
44
|
+
option.setAttribute("aria-selected", selected)
|
45
|
+
}
|
46
|
+
|
41
47
|
_deselect() {
|
42
48
|
const option = this._selectedOptionElement
|
43
|
-
|
44
|
-
if (this.hasSelectedClass) option?.classList.remove(this.selectedClass)
|
45
|
-
this._commitSelection(option, { selected: false })
|
49
|
+
if (option) this._commitSelection(option, { selected: false })
|
46
50
|
}
|
47
51
|
|
48
52
|
_selectNew(query) {
|
@@ -56,7 +60,17 @@ Combobox.Selection = Base => class extends Base {
|
|
56
60
|
this._select(option, { force: true })
|
57
61
|
}
|
58
62
|
|
59
|
-
|
60
|
-
this.
|
63
|
+
_preselectOption() {
|
64
|
+
if (this._hasValueButNoSelection && this._allOptions.length < 100) {
|
65
|
+
const option = this._allOptions.find(option => {
|
66
|
+
return option.dataset.value === this.hiddenFieldTarget.value
|
67
|
+
})
|
68
|
+
|
69
|
+
if (option) this._markSelected(option, { selected: true })
|
70
|
+
}
|
71
|
+
}
|
72
|
+
|
73
|
+
get _hasValueButNoSelection() {
|
74
|
+
return this.hiddenFieldTarget.value && !this._selectedOptionElement
|
61
75
|
}
|
62
76
|
}
|
@@ -21,8 +21,11 @@ Combobox.Toggle = Base => class extends Base {
|
|
21
21
|
}
|
22
22
|
}
|
23
23
|
|
24
|
-
closeOnClickOutside(
|
24
|
+
closeOnClickOutside(event) {
|
25
|
+
const target = event.target
|
26
|
+
|
25
27
|
if (this.element.contains(target) && !this._isDialogDismisser(target)) return
|
28
|
+
if (this._withinElementBounds(event)) return
|
26
29
|
|
27
30
|
this.close()
|
28
31
|
}
|
@@ -35,6 +38,17 @@ Combobox.Toggle = Base => class extends Base {
|
|
35
38
|
this.close()
|
36
39
|
}
|
37
40
|
|
41
|
+
// Some browser extensions like 1Password overlay elements on top of the combobox.
|
42
|
+
// Hovering over these elements emits a click event for some reason.
|
43
|
+
// These events don't contain any telling information, so we use `_withinElementBounds`
|
44
|
+
// as an alternative to check whether the click is legitimate.
|
45
|
+
_withinElementBounds(event) {
|
46
|
+
const { left, right, top, bottom } = this.element.getBoundingClientRect()
|
47
|
+
const { clientX, clientY } = event
|
48
|
+
|
49
|
+
return clientX >= left && clientX <= right && clientY >= top && clientY <= bottom
|
50
|
+
}
|
51
|
+
|
38
52
|
_ensureSelection() {
|
39
53
|
if (!this._isValidNewOption(this._actingCombobox.value, { ignoreAutocomplete: true })) {
|
40
54
|
this._select(this._selectedOptionElement, { force: true })
|
@@ -49,7 +63,9 @@ Combobox.Toggle = Base => class extends Base {
|
|
49
63
|
return target.closest("dialog") && target.role != "combobox"
|
50
64
|
}
|
51
65
|
|
52
|
-
|
66
|
+
_expand() {
|
67
|
+
if (this._preselectOnExpansion) this._preselectOption()
|
68
|
+
|
53
69
|
if (this._autocompletesList && this._smallViewport) {
|
54
70
|
this._openInDialog()
|
55
71
|
} else {
|
@@ -101,4 +117,8 @@ Combobox.Toggle = Base => class extends Base {
|
|
101
117
|
get _isOpen() {
|
102
118
|
return this.expandedValue
|
103
119
|
}
|
120
|
+
|
121
|
+
get _preselectOnExpansion() {
|
122
|
+
return !this._isAsync // async comboboxes preselect based on callbacks
|
123
|
+
}
|
104
124
|
}
|
@@ -15,10 +15,9 @@ module HotwireCombobox
|
|
15
15
|
end
|
16
16
|
hw_alias :hw_combobox_style_tag
|
17
17
|
|
18
|
-
def hw_combobox_tag(
|
19
|
-
options =
|
20
|
-
|
21
|
-
component = HotwireCombobox::Component.new self, *args, options: options, async_src: src, **kwargs
|
18
|
+
def hw_combobox_tag(name, options_or_src = [], render_in: {}, **kwargs)
|
19
|
+
options, src = hw_extract_options_and_src(options_or_src, render_in)
|
20
|
+
component = HotwireCombobox::Component.new self, name, options: options, async_src: src, **kwargs
|
22
21
|
|
23
22
|
render "hotwire_combobox/combobox", component: component
|
24
23
|
end
|
@@ -39,20 +38,32 @@ module HotwireCombobox
|
|
39
38
|
end
|
40
39
|
hw_alias :hw_combobox_options
|
41
40
|
|
42
|
-
def hw_paginated_combobox_options(options, for_id:, src
|
43
|
-
this_page = render("hotwire_combobox/paginated_options", for_id: for_id, options: hw_combobox_options(options, render_in: render_in, **methods)
|
44
|
-
next_page = render("hotwire_combobox/next_page", src: src, next_page: next_page
|
41
|
+
def hw_paginated_combobox_options(options, for_id:, src: request.path, next_page: nil, render_in: {}, **methods)
|
42
|
+
this_page = render("hotwire_combobox/paginated_options", for_id: for_id, options: hw_combobox_options(options, render_in: render_in, **methods))
|
43
|
+
next_page = render("hotwire_combobox/next_page", for_id: for_id, src: src, next_page: next_page)
|
45
44
|
|
46
45
|
safe_join [ this_page, next_page ]
|
47
46
|
end
|
48
47
|
hw_alias :hw_paginated_combobox_options
|
49
48
|
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
49
|
+
protected # library use only
|
50
|
+
def hw_listbox_options_id(id)
|
51
|
+
"#{id}-hw-listbox__options"
|
52
|
+
end
|
53
|
+
|
54
|
+
def hw_pagination_frame_id(id)
|
55
|
+
"#{id}__hw_combobox_pagination"
|
56
|
+
end
|
54
57
|
|
55
58
|
private
|
59
|
+
def hw_extract_options_and_src(options_or_src, render_in)
|
60
|
+
if options_or_src.is_a? String
|
61
|
+
[ [], hw_uri_with_params(options_or_src, format: :turbo_stream) ]
|
62
|
+
else
|
63
|
+
[ hw_combobox_options(options_or_src, render_in: render_in), nil ]
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
56
67
|
def hw_uri_with_params(url_or_path, **params)
|
57
68
|
URI.parse(url_or_path).tap do |url_or_path|
|
58
69
|
query = URI.decode_www_form(url_or_path.query || "").to_h.merge(params)
|
@@ -1,23 +1,29 @@
|
|
1
1
|
class HotwireCombobox::Component
|
2
2
|
attr_reader :async_src, :options, :dialog_label
|
3
3
|
|
4
|
-
def initialize
|
5
|
-
view, name,
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
4
|
+
def initialize \
|
5
|
+
view, name,
|
6
|
+
association_name: nil,
|
7
|
+
async_src: nil,
|
8
|
+
autocomplete: :both,
|
9
|
+
data: {},
|
10
|
+
dialog_label: nil,
|
11
|
+
form: nil,
|
12
|
+
id: nil,
|
13
|
+
input: {},
|
14
|
+
name_when_new: nil,
|
15
|
+
open: false,
|
16
|
+
options: [],
|
17
|
+
small_width: "640px",
|
18
|
+
value: nil,
|
19
|
+
**rest
|
17
20
|
@view, @autocomplete, @id, @name, @value, @form, @async_src,
|
18
21
|
@name_when_new, @open, @data, @small_width, @options, @dialog_label =
|
19
22
|
view, autocomplete, id, name, value, form, async_src,
|
20
23
|
name_when_new, open, data, small_width, options, dialog_label
|
24
|
+
|
25
|
+
@combobox_attrs = input.reverse_merge(rest).with_indifferent_access
|
26
|
+
@association_name = association_name || infer_association_name
|
21
27
|
end
|
22
28
|
|
23
29
|
def fieldset_attrs
|
@@ -125,15 +131,23 @@ class HotwireCombobox::Component
|
|
125
131
|
end
|
126
132
|
|
127
133
|
def pagination_attrs
|
128
|
-
{ src: async_src }
|
134
|
+
{ for_id: hidden_field_id, src: async_src }
|
129
135
|
end
|
130
136
|
|
131
137
|
private
|
132
138
|
attr_reader :view, :autocomplete, :id, :name, :value, :form,
|
133
|
-
:name_when_new, :open, :data, :combobox_attrs, :small_width
|
139
|
+
:name_when_new, :open, :data, :combobox_attrs, :small_width,
|
140
|
+
:association_name
|
141
|
+
|
142
|
+
def infer_association_name
|
143
|
+
if name.to_s.include?("_id")
|
144
|
+
name.to_s.sub(/_id\z/, "")
|
145
|
+
end
|
146
|
+
end
|
134
147
|
|
135
148
|
def fieldset_data
|
136
149
|
data.reverse_merge \
|
150
|
+
pagination_id: hidden_field_id,
|
137
151
|
controller: view.token_list("hw-combobox", data[:controller]),
|
138
152
|
hw_combobox_expanded_value: open,
|
139
153
|
hw_combobox_name_when_new_value: name_when_new,
|
@@ -141,11 +155,30 @@ class HotwireCombobox::Component
|
|
141
155
|
hw_combobox_autocomplete_value: autocomplete,
|
142
156
|
hw_combobox_small_viewport_max_width_value: small_width,
|
143
157
|
hw_combobox_async_src_value: async_src,
|
158
|
+
hw_combobox_prefilled_display_value: prefilled_display,
|
144
159
|
hw_combobox_filterable_attribute_value: "data-filterable-as",
|
145
160
|
hw_combobox_autocompletable_attribute_value: "data-autocompletable-as",
|
146
161
|
hw_combobox_selected_class: "hw-combobox__option--selected"
|
147
162
|
end
|
148
163
|
|
164
|
+
def prefilled_display
|
165
|
+
if async_src && associated_object
|
166
|
+
associated_object.to_combobox_display
|
167
|
+
elsif value
|
168
|
+
options.find { |option| option.value == value }&.content
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
def associated_object
|
173
|
+
@associated_object ||= if association_exists?
|
174
|
+
form.object.public_send association_name
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
def association_exists?
|
179
|
+
form&.object&.class&.reflect_on_association(association_name).present?
|
180
|
+
end
|
181
|
+
|
149
182
|
|
150
183
|
def hidden_field_id
|
151
184
|
id || form&.field_id(name)
|
@@ -180,7 +213,8 @@ class HotwireCombobox::Component
|
|
180
213
|
keydown->hw-combobox#navigate
|
181
214
|
click@window->hw-combobox#closeOnClickOutside
|
182
215
|
focusin@window->hw-combobox#closeOnFocusOutside".squish,
|
183
|
-
hw_combobox_target: "combobox"
|
216
|
+
hw_combobox_target: "combobox",
|
217
|
+
pagination_id: hidden_field_id
|
184
218
|
end
|
185
219
|
|
186
220
|
def input_aria
|
@@ -3,8 +3,16 @@ class HotwireCombobox::Listbox::Option
|
|
3
3
|
@option = option.is_a?(Hash) ? Data.new(**option) : option
|
4
4
|
end
|
5
5
|
|
6
|
-
def render_in(
|
7
|
-
|
6
|
+
def render_in(view)
|
7
|
+
view.tag.li content, **options
|
8
|
+
end
|
9
|
+
|
10
|
+
def value
|
11
|
+
option.try(:value) || option.id
|
12
|
+
end
|
13
|
+
|
14
|
+
def content
|
15
|
+
option.try(:content) || option.try(:display)
|
8
16
|
end
|
9
17
|
|
10
18
|
private
|
@@ -12,10 +20,6 @@ class HotwireCombobox::Listbox::Option
|
|
12
20
|
|
13
21
|
attr_reader :option
|
14
22
|
|
15
|
-
def content
|
16
|
-
option.try(:content) || option.try(:display)
|
17
|
-
end
|
18
|
-
|
19
23
|
def options
|
20
24
|
{
|
21
25
|
id: id,
|
@@ -45,8 +49,4 @@ class HotwireCombobox::Listbox::Option
|
|
45
49
|
def autocompletable_as
|
46
50
|
option.try(:autocompletable_as) || option.try(:display)
|
47
51
|
end
|
48
|
-
|
49
|
-
def value
|
50
|
-
option.try(:value) || option.id
|
51
|
-
end
|
52
52
|
end
|
@@ -1,5 +1,5 @@
|
|
1
|
-
<%# locals: (next_page:, src:) -%>
|
1
|
+
<%# locals: (for_id:, next_page:, src:) -%>
|
2
2
|
|
3
|
-
<%= turbo_stream.replace
|
4
|
-
<%= render "hotwire_combobox/pagination", src: hw_combobox_next_page_uri(src, next_page) %>
|
3
|
+
<%= turbo_stream.replace hw_pagination_frame_id(for_id) do %>
|
4
|
+
<%= render "hotwire_combobox/pagination", for_id: for_id, src: hw_combobox_next_page_uri(src, next_page) %>
|
5
5
|
<% end %>
|
@@ -1,3 +1,4 @@
|
|
1
|
-
<%# locals: (src:) -%>
|
1
|
+
<%# locals: (for_id:, src:) -%>
|
2
2
|
|
3
|
-
<%= turbo_frame_tag
|
3
|
+
<%= turbo_frame_tag hw_pagination_frame_id(for_id), src: src, loading: :lazy,
|
4
|
+
data: { hw_combobox_target: "paginationFrame" } %>
|
@@ -8,8 +8,8 @@ module HotwireCombobox
|
|
8
8
|
|
9
9
|
unless HotwireCombobox.bypass_convenience_methods?
|
10
10
|
module FormBuilderExtensions
|
11
|
-
def combobox(*args, **kwargs
|
12
|
-
@template.hw_combobox_tag *args, **kwargs.merge(form: self)
|
11
|
+
def combobox(*args, **kwargs)
|
12
|
+
@template.hw_combobox_tag *args, **kwargs.merge(form: self)
|
13
13
|
end
|
14
14
|
end
|
15
15
|
|
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.15
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Jose Farias
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2024-
|
11
|
+
date: 2024-02-03 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rails
|
@@ -80,7 +80,7 @@ dependencies:
|
|
80
80
|
- - ">="
|
81
81
|
- !ruby/object:Gem::Version
|
82
82
|
version: 0.0.11
|
83
|
-
description: A combobox implementation for Ruby on Rails apps
|
83
|
+
description: A combobox implementation for Ruby on Rails apps using Hotwire.
|
84
84
|
email:
|
85
85
|
- jose@farias.mx
|
86
86
|
executables: []
|
@@ -149,5 +149,5 @@ requirements: []
|
|
149
149
|
rubygems_version: 3.4.18
|
150
150
|
signing_key:
|
151
151
|
specification_version: 4
|
152
|
-
summary:
|
152
|
+
summary: Autocomplete for Rails apps using Hotwire
|
153
153
|
test_files: []
|