Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Gemfile.lock
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
PATH
remote: .
specs:
advanced_select (0.1.9)
advanced_select (0.1.10)
actionview (>= 7.1)
railties (>= 7.1)
stimulus-rails (>= 1.3)
Expand Down
47 changes: 47 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ AdvancedSelect is a small Rails engine for rendering an advanced select input wi
- [Custom Option Content](#custom-option-content)
- [Option Contract](#option-contract)
- [Events](#events)
- [Disabled State](#disabled-state)
- [API Reference](#api-reference)
- [Local Development](#local-development)
- [i18n](#i18n)
Expand Down Expand Up @@ -791,6 +792,49 @@ const select = application.getControllerForElementAndIdentifier(element, "advanc
select.currentValue // => ["3", "7"]
```

### Disabled State

Pass `disabled: true` to lock a field. It still renders its current selection —
the point is to show the value, not to hide it — but it cannot be opened,
searched, or cleared, and its value is **left out of the form**:

```erb
<%= advanced_select_tag(
"record[item_id]",
id: "record_item_id",
selected: selected_option,
options: options,
placeholder: "Choose an item",
disabled: true
) %>
```

This matches a native `<select disabled>`: the browser omits a disabled field
from the submitted params, so an update never writes a value the user was not
allowed to change.

The trigger carries the `disabled` attribute, so mouse and keyboard are both
blocked by the browser rather than by CSS alone. The root element takes the
`disabled` class from the class map.

To toggle the state after render — from a Stimulus controller, or from a Turbo
Stream that flips the attribute — set the value on the root element:

```js
element.dataset.advancedSelectDisabledValue = "true"
```

The controller reacts on its own: it disables the hidden inputs, applies the
class, hides the clear control, and closes the dropdown if it happens to be
open. The same thing is available as a method:

```js
const select = application.getControllerForElementAndIdentifier(element, "advanced-select")

select.disable()
select.enable()
```

### API Reference

`advanced_select_tag`:
Expand All @@ -806,6 +850,7 @@ advanced_select_tag(
multiple: false,
searchable: true,
add_mode: false,
disabled: false,
dependent_fields: {},
include_hidden: true,
auto_select_single: true,
Expand All @@ -823,6 +868,8 @@ advanced_select_tag(

`tooltip:` / `tooltip_partial:` enable an optional hover tooltip on the trigger (see [Selection Tooltip](#selection-tooltip)).

`disabled:` locks the field (see [Disabled State](#disabled-state)).

`advanced_select_options_tag`:

```ruby
Expand Down
10 changes: 10 additions & 0 deletions app/assets/stylesheets/advanced_select/advanced_select.css
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,16 @@
display: none !important;
}

.ui-advanced-select-disabled .ui-advanced-select-trigger {
background: #f3f4f6;
color: #6b7280;
cursor: not-allowed;
}

.ui-advanced-select-disabled .ui-advanced-select-clear {
display: none;
}

.ui-advanced-select-trigger {
align-items: center;
background: #ffffff;
Expand Down
42 changes: 41 additions & 1 deletion app/javascript/advanced_select/advanced_select_controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export default class extends Controller {
autoSelectSingle: { type: Boolean, default: true },
delay: { type: Number, default: 200 },
dependentFields: Object,
disabled: Boolean,
eager: { type: Boolean, default: true },
emptyText: String,
errorText: String,
Expand Down Expand Up @@ -65,7 +66,40 @@ export default class extends Controller {
this.expanded ? this.close() : this.open()
}

disable() {
this.disabledValue = true
}

enable() {
this.disabledValue = false
}

disabledValueChanged() {
if (!this.hasTriggerTarget) {
return
}

const disabled = this.disabledValue
const classes = this.classList(this.element.dataset.advancedSelectDisabledClass || "ui-advanced-select-disabled")

this.triggerTarget.disabled = disabled
this.hiddenFieldsTarget.querySelectorAll("input").forEach((input) => { input.disabled = disabled })
this.clearTarget.classList.toggle("hidden", disabled || this.selectedValue.length === 0)

if (classes.length) {
disabled ? this.element.classList.add(...classes) : this.element.classList.remove(...classes)
}

if (disabled && this.expanded) {
this.close()
}
}

open() {
if (this.disabledValue) {
return
}

this.hideTooltip(true)
this.dropdownTarget.classList.remove("hidden")
this.triggerTarget.setAttribute("aria-expanded", "true")
Expand Down Expand Up @@ -120,6 +154,11 @@ export default class extends Controller {
clear(event) {
event.preventDefault()
event.stopPropagation()

if (this.disabledValue) {
return
}

this.selectedValue = []
this.renderSelection()
this.close()
Expand Down Expand Up @@ -362,7 +401,7 @@ export default class extends Controller {
this.renderTooltip()
this.renderOptionsState()
this.caretTarget.classList.toggle("hidden", this.selectedValue.length > 0)
this.clearTarget.classList.toggle("hidden", this.selectedValue.length === 0)
this.clearTarget.classList.toggle("hidden", this.disabledValue || this.selectedValue.length === 0)
this.dispatchValueChange()
}

Expand Down Expand Up @@ -515,6 +554,7 @@ export default class extends Controller {
input.type = "hidden"
input.name = this.nameValue
input.value = option ? option.value || option.id : ""
input.disabled = this.disabledValue

if (!this.multipleValue) {
input.id = this.inputIdValue
Expand Down
13 changes: 8 additions & 5 deletions app/views/advanced_select/_select.html.erb
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
<div class="<%= advanced_select_class(class_map, :root) %>"
<div class="<%= advanced_select_class(class_map, :root, (:disabled if disabled)) %>"
data-controller="advanced-select"
data-advanced-select-disabled-value="<%= disabled %>"
data-advanced-select-disabled-class="<%= advanced_select_state_class(class_map, :disabled) %>"
data-advanced-select-url-value="<%= options_url %>"
data-advanced-select-target-id-value="<%= target_id %>"
data-advanced-select-name-value="<%= name %>"
Expand Down Expand Up @@ -31,13 +33,13 @@
<div data-advanced-select-target="hiddenFields">
<% if multiple %>
<% if include_hidden %>
<%= hidden_field_tag name, "", id: nil %>
<%= hidden_field_tag name, "", id: nil, disabled: disabled %>
<% end %>
<% selected_options.each do |option| %>
<%= hidden_field_tag name, option.fetch(:value, option.fetch(:id)) %>
<%= hidden_field_tag name, option.fetch(:value, option.fetch(:id)), disabled: disabled %>
<% end %>
<% else %>
<%= hidden_field_tag name, selected_options.first&.fetch(:value, selected_options.first&.fetch(:id)), id: id %>
<%= hidden_field_tag name, selected_options.first&.fetch(:value, selected_options.first&.fetch(:id)), id: id, disabled: disabled %>
<% end %>
</div>

Expand All @@ -49,13 +51,14 @@
aria-haspopup="listbox"
aria-expanded="false"
aria-controls="<%= "#{id}_dropdown" %>"
<%= "disabled" if disabled %>
data-advanced-select-target="trigger"
data-action="advanced-select#toggle keydown->advanced-select#keydown<%= " mouseenter->advanced-select#showTooltip mouseleave->advanced-select#hideTooltip" if tooltip_enabled %>">
<span id="<%= "#{id}_summary" %>" class="<%= advanced_select_class(class_map, :summary) %>" data-advanced-select-target="summary">
<%= render partial: "advanced_select/summary", locals: { selected_options: selected_options, multiple: multiple, placeholder: placeholder, summary_mode: summary_mode, class_map: class_map } %>
</span>
<span id="<%= "#{id}_caret" %>" class="<%= [advanced_select_class(class_map, :caret), ("hidden" if selected_options.any?)].compact.join(" ") %>" data-advanced-select-target="caret">&#8964;</span>
<span id="<%= "#{id}_clear" %>" class="<%= [advanced_select_class(class_map, :clear), ("hidden" if selected_options.empty?)].compact.join(" ") %>" data-advanced-select-target="clear" data-action="click->advanced-select#clear">&times;</span>
<span id="<%= "#{id}_clear" %>" class="<%= [advanced_select_class(class_map, :clear), ("hidden" if disabled || selected_options.empty?)].compact.join(" ") %>" data-advanced-select-target="clear" data-action="click->advanced-select#clear">&times;</span>
</button>

<% if tooltip_enabled %>
Expand Down
1 change: 1 addition & 0 deletions lib/advanced_select/class_map.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ module AdvancedSelect
class ClassMap
DEFAULTS = {
root: "ui-advanced-select",
disabled: "ui-advanced-select-disabled",
trigger: "ui-advanced-select-trigger",
summary: "ui-advanced-select-summary",
placeholder: "ui-advanced-select-placeholder",
Expand Down
3 changes: 2 additions & 1 deletion lib/advanced_select/helper.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module AdvancedSelect
module Helper
def advanced_select_tag(name, id:, selected:, options:, placeholder:, options_url: nil, multiple: false, searchable: true, add_mode: false, dependent_fields: {}, include_hidden: true, auto_select_single: true, eager: true, summary_mode: :tokens, tooltip: false, tooltip_partial: nil, option_content_partial: nil, classes: {}, append_classes: {})
def advanced_select_tag(name, id:, selected:, options:, placeholder:, options_url: nil, multiple: false, searchable: true, add_mode: false, disabled: false, dependent_fields: {}, include_hidden: true, auto_select_single: true, eager: true, summary_mode: :tokens, tooltip: false, tooltip_partial: nil, option_content_partial: nil, classes: {}, append_classes: {})
selected_options = advanced_select_selected_options(selected)
class_map = advanced_select_class_map(classes, append_classes)

Expand All @@ -14,6 +14,7 @@ def advanced_select_tag(name, id:, selected:, options:, placeholder:, options_ur
multiple: multiple,
searchable: searchable,
add_mode: add_mode,
disabled: disabled,
dependent_fields: dependent_fields,
include_hidden: include_hidden,
auto_select_single: auto_select_single,
Expand Down
2 changes: 1 addition & 1 deletion lib/advanced_select/version.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
module AdvancedSelect
VERSION = "0.1.9"
VERSION = "0.1.10"
end
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,16 @@
display: none !important;
}

.ui-advanced-select-disabled .ui-advanced-select-trigger {
background: #f3f4f6;
color: #6b7280;
cursor: not-allowed;
}

.ui-advanced-select-disabled .ui-advanced-select-clear {
display: none;
}

.ui-advanced-select-trigger {
align-items: center;
background: #ffffff;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export default class extends Controller {
autoSelectSingle: { type: Boolean, default: true },
delay: { type: Number, default: 200 },
dependentFields: Object,
disabled: Boolean,
eager: { type: Boolean, default: true },
emptyText: String,
errorText: String,
Expand Down Expand Up @@ -65,7 +66,40 @@ export default class extends Controller {
this.expanded ? this.close() : this.open()
}

disable() {
this.disabledValue = true
}

enable() {
this.disabledValue = false
}

disabledValueChanged() {
if (!this.hasTriggerTarget) {
return
}

const disabled = this.disabledValue
const classes = this.classList(this.element.dataset.advancedSelectDisabledClass || "ui-advanced-select-disabled")

this.triggerTarget.disabled = disabled
this.hiddenFieldsTarget.querySelectorAll("input").forEach((input) => { input.disabled = disabled })
this.clearTarget.classList.toggle("hidden", disabled || this.selectedValue.length === 0)

if (classes.length) {
disabled ? this.element.classList.add(...classes) : this.element.classList.remove(...classes)
}

if (disabled && this.expanded) {
this.close()
}
}

open() {
if (this.disabledValue) {
return
}

this.hideTooltip(true)
this.dropdownTarget.classList.remove("hidden")
this.triggerTarget.setAttribute("aria-expanded", "true")
Expand Down Expand Up @@ -120,6 +154,11 @@ export default class extends Controller {
clear(event) {
event.preventDefault()
event.stopPropagation()

if (this.disabledValue) {
return
}

this.selectedValue = []
this.renderSelection()
this.close()
Expand Down Expand Up @@ -362,7 +401,7 @@ export default class extends Controller {
this.renderTooltip()
this.renderOptionsState()
this.caretTarget.classList.toggle("hidden", this.selectedValue.length > 0)
this.clearTarget.classList.toggle("hidden", this.selectedValue.length === 0)
this.clearTarget.classList.toggle("hidden", this.disabledValue || this.selectedValue.length === 0)
this.dispatchValueChange()
}

Expand Down Expand Up @@ -515,6 +554,7 @@ export default class extends Controller {
input.type = "hidden"
input.name = this.nameValue
input.value = option ? option.value || option.id : ""
input.disabled = this.disabledValue

if (!this.multipleValue) {
input.id = this.inputIdValue
Expand Down
13 changes: 13 additions & 0 deletions test/dummy/app/views/advanced_select_examples/show.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -270,3 +270,16 @@
classes: styled_advanced_select_classes,
append_classes: styled_advanced_select_append_classes
) %>

<%= advanced_select_tag(
"example[disabled_id]",
id: "example_disabled_id",
selected: { id: "disabled-1", label: "Disabled One" },
options: [
{ id: "disabled-1", label: "Disabled One" },
{ id: "disabled-2", label: "Disabled Two" }
],
placeholder: "Choose disabled item",
disabled: true,
searchable: false
) %>
Loading
Loading