From af7505ad2a691608566d669b5cac60dc824a4aea Mon Sep 17 00:00:00 2001 From: "ulsyemr@gmail.com" Date: Wed, 12 Aug 2026 14:33:54 +0300 Subject: [PATCH 1/2] Broadcast advanced-select:change from the root element --- README.md | 46 +++++++++++++ .../advanced_select_controller.js | 15 ++++ .../templates/advanced_select_controller.js | 15 ++++ .../advanced_select_interaction_test.rb | 68 +++++++++++++++++++ 4 files changed, 144 insertions(+) diff --git a/README.md b/README.md index 7b25b16..c719da4 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,7 @@ AdvancedSelect is a small Rails engine for rendering an advanced select input wi - [Dependent Fields](#dependent-fields) - [Custom Option Content](#custom-option-content) - [Option Contract](#option-contract) +- [Events](#events) - [API Reference](#api-reference) - [Local Development](#local-development) - [i18n](#i18n) @@ -745,6 +746,51 @@ Grouped options use this shape: ] ``` +### Events + +Every value change dispatches two events: + +| Event | Target | Purpose | +| --- | --- | --- | +| `change` | the first hidden input | native form behaviour and [dependent fields](#dependent-fields) | +| `advanced-select:change` | the root element | application listeners that need the whole selection | + +Both bubble. `advanced-select:change` carries a `detail` payload: + +```js +{ + name: "record[item_ids][]", + value: ["3", "7"], + options: [ + { id: "3", value: "3", label: "Item A", display_label: "Item A" }, + { id: "7", value: "7", label: "Item B", display_label: "Item B" } + ] +} +``` + +`value` is the submit value — the option's `value` when it defines one, otherwise its `id`. It is an +array for multiple selects, a string for single selects, and empty when nothing is selected. A +multiple select lists the most recently chosen option first, matching the order of its hidden +inputs. + +Bind it like any other event: + +```erb +data-action="advanced-select:change->my-controller#refresh" +``` + +Prefer it over `change` on multiple selects. `change` is dispatched from the first hidden input, so +it does not fire at all once the last value is cleared from a field rendered with +`include_hidden: false` — at that point the field has no inputs left. + +The current value is also readable from the controller: + +```js +const select = application.getControllerForElementAndIdentifier(element, "advanced-select") + +select.currentValue // => ["3", "7"] +``` + ### API Reference `advanced_select_tag`: diff --git a/app/javascript/advanced_select/advanced_select_controller.js b/app/javascript/advanced_select/advanced_select_controller.js index fe8eddc..a8bdf3c 100644 --- a/app/javascript/advanced_select/advanced_select_controller.js +++ b/app/javascript/advanced_select/advanced_select_controller.js @@ -371,6 +371,21 @@ export default class extends Controller { if (input) { input.dispatchEvent(new Event("change", { bubbles: true })) } + + this.element.dispatchEvent(new CustomEvent("advanced-select:change", { + bubbles: true, + detail: { + name: this.nameValue, + value: this.currentValue, + options: [...this.selectedValue] + } + })) + } + + get currentValue() { + const values = this.selectedValue.map((option) => option.value || option.id) + + return this.multipleValue ? values : (values[0] || "") } renderOptionsState() { diff --git a/lib/generators/advanced_select/install/templates/advanced_select_controller.js b/lib/generators/advanced_select/install/templates/advanced_select_controller.js index fe8eddc..a8bdf3c 100644 --- a/lib/generators/advanced_select/install/templates/advanced_select_controller.js +++ b/lib/generators/advanced_select/install/templates/advanced_select_controller.js @@ -371,6 +371,21 @@ export default class extends Controller { if (input) { input.dispatchEvent(new Event("change", { bubbles: true })) } + + this.element.dispatchEvent(new CustomEvent("advanced-select:change", { + bubbles: true, + detail: { + name: this.nameValue, + value: this.currentValue, + options: [...this.selectedValue] + } + })) + } + + get currentValue() { + const values = this.selectedValue.map((option) => option.value || option.id) + + return this.multipleValue ? values : (values[0] || "") } renderOptionsState() { diff --git a/test/system/advanced_select_interaction_test.rb b/test/system/advanced_select_interaction_test.rb index bffbee0..27000af 100644 --- a/test/system/advanced_select_interaction_test.rb +++ b/test/system/advanced_select_interaction_test.rb @@ -340,8 +340,76 @@ class AdvancedSelectInteractionTest < ApplicationSystemTestCase end end + test "broadcasts advanced-select:change with the selected value" do + visit root_path + record_advanced_select_events + + find("#example_item_id_trigger").click + find("#example_item_id_options button", text: "Local One").click + + assert_selector "#example_item_id_summary", text: "Local One" + + event = advanced_select_events.last + + assert_equal 1, advanced_select_events.size + assert_equal "example[item_id]", event["name"] + assert_equal "local-1", event["value"] + assert_equal ["Local One"], event["options"].map { |option| option["label"] } + end + + test "broadcasts the submit value rather than the option id" do + visit root_path + record_advanced_select_events + + find("#example_submit_id_trigger").click + find("#example_submit_id_options button", text: "Submit Item").click + + assert_selector "#example_submit_id_summary", text: "Submit Item" + assert_equal "submit-7", advanced_select_events.last["value"] + end + + test "broadcasts an array of values for multiple selects" do + visit root_path + record_advanced_select_events + + find("#example_multiple_ids_trigger").click + find("#example_multiple_ids_options button", text: "Multi One").click + find("#example_multiple_ids_options button", text: "Multi Two").click + + assert_selector "#example_multiple_ids_summary", text: "Multi Two" + assert_equal [["multi-1"], %w[multi-2 multi-1]], advanced_select_events.map { |event| event["value"] } + end + + test "broadcasts when the last value is cleared without a blank hidden field" do + visit root_path + + find("#example_multiple_ids_without_blank_trigger").click + find("#example_multiple_ids_without_blank_options button", text: "Multi One").click + + assert_selector "input[name='example[multiple_ids_without_blank][]'][value='multi-1']", visible: false + + record_advanced_select_events + find("#example_multiple_ids_without_blank_clear").click + + assert_no_selector "input[name='example[multiple_ids_without_blank][]']", visible: false + assert_equal [[]], advanced_select_events.map { |event| event["value"] } + end + private + def record_advanced_select_events + page.execute_script(<<~JS) + window.__advancedSelectEvents = [] + document.addEventListener("advanced-select:change", (event) => { + window.__advancedSelectEvents.push(event.detail) + }) + JS + end + + def advanced_select_events + page.evaluate_script("window.__advancedSelectEvents") + end + def assert_selected_option_check(select_id, text) option = find("##{select_id}_options button[aria-selected='true']", text: text) From 858a91e86f42ed83599d70c37b50c0ec49f7ef38 Mon Sep 17 00:00:00 2001 From: "ulsyemr@gmail.com" Date: Wed, 12 Aug 2026 15:53:58 +0300 Subject: [PATCH 2/2] bump version to 0.1.9 --- lib/advanced_select/version.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/advanced_select/version.rb b/lib/advanced_select/version.rb index 41ba1b5..6ccbe37 100644 --- a/lib/advanced_select/version.rb +++ b/lib/advanced_select/version.rb @@ -1,3 +1,3 @@ module AdvancedSelect - VERSION = "0.1.8" + VERSION = "0.1.9" end