From 81b60eefad9219e9118675beb1a6a627acc4cf04 Mon Sep 17 00:00:00 2001 From: Emi Matchu Date: Fri, 6 Feb 2026 11:26:51 -0800 Subject: [PATCH] [WV2] Unify auto-submit behaviors into a shared web component --- app/assets/javascripts/auto-submit-form.js | 27 +++++++++++++++++ app/assets/javascripts/items/show.js | 2 +- app/assets/javascripts/pose-picker.js | 27 ++--------------- .../javascripts/species-color-picker.js | 28 ------------------ app/assets/stylesheets/items/show.sass | 6 ++-- app/assets/stylesheets/wardrobe/show.css | 5 ++-- app/views/items/show.html.haml | 29 ++++++++++--------- .../appearance/_pose_picker.html.haml | 6 ++-- .../_species_color_picker.html.haml | 25 ++++++++-------- app/views/wardrobe/show.html.haml | 2 +- docs/wardrobe-v2-migration.md | 2 +- 11 files changed, 71 insertions(+), 88 deletions(-) create mode 100644 app/assets/javascripts/auto-submit-form.js delete mode 100644 app/assets/javascripts/species-color-picker.js diff --git a/app/assets/javascripts/auto-submit-form.js b/app/assets/javascripts/auto-submit-form.js new file mode 100644 index 00000000..62eb4321 --- /dev/null +++ b/app/assets/javascripts/auto-submit-form.js @@ -0,0 +1,27 @@ +/** + * AutoSubmitForm web component + * + * Generic progressive enhancement for forms that should auto-submit on change: + * - Listens for `change` events on descendant form inputs + * - Calls `requestSubmit()` on the nearest `
` + * - Exposes `:state(auto-loading)` to hide fallback submit buttons via CSS + */ +class AutoSubmitForm extends HTMLElement { + #internals; + + constructor() { + super(); + this.#internals = this.attachInternals(); + } + + connectedCallback() { + this.addEventListener("change", this.#handleChange); + this.#internals.states.add("auto-loading"); + } + + #handleChange(e) { + e.target.closest("form")?.requestSubmit(); + } +} + +customElements.define("auto-submit-form", AutoSubmitForm); diff --git a/app/assets/javascripts/items/show.js b/app/assets/javascripts/items/show.js index d5dfb960..0d4fb551 100644 --- a/app/assets/javascripts/items/show.js +++ b/app/assets/javascripts/items/show.js @@ -4,7 +4,7 @@ document.addEventListener("change", (e) => { try { const mainPickerForm = document.querySelector( - "#item-preview species-color-picker form", + "#item-preview .species-color-picker form", ); const mainSpeciesField = mainPickerForm.querySelector( "[name='preview[species_id]']", diff --git a/app/assets/javascripts/pose-picker.js b/app/assets/javascripts/pose-picker.js index 131277a7..a0334465 100644 --- a/app/assets/javascripts/pose-picker.js +++ b/app/assets/javascripts/pose-picker.js @@ -1,25 +1,13 @@ /** - * PosePicker web component + * PosePickerPopover web component * - * Progressive enhancement for pose picker forms: - * - Auto-submits the form when a pose is selected (if JS is enabled) - * - Shows a submit button as fallback (if JS is disabled or slow to load) - * - Uses Custom Element internals API to communicate state to CSS + * Scrolls the selected style into view when the style picker list becomes + * visible (e.g. tab switch or popover open). */ class PosePickerPopover extends HTMLElement { - #internals; #styleListObserver; - constructor() { - super(); - this.#internals = this.attachInternals(); - } - connectedCallback() { - // Listen for changes to auto-submit the form, then tell CSS about it! - this.addEventListener("change", this.#handleChange); - this.#internals.states.add("auto-loading"); - // When the style picker list becomes visible (e.g. tab switch or // popover open), scroll the selected style into view. const styleList = this.querySelector(".style-picker-list"); @@ -36,18 +24,9 @@ class PosePickerPopover extends HTMLElement { } } - disconnectedCallback() { this.#styleListObserver?.disconnect(); } - - #handleChange(e) { - // Only auto-submit if a radio button was changed - if (e.target.type === "radio") { - e.target.closest("form").requestSubmit(); - } - } - } customElements.define("pose-picker-popover", PosePickerPopover); diff --git a/app/assets/javascripts/species-color-picker.js b/app/assets/javascripts/species-color-picker.js deleted file mode 100644 index 6ed1355b..00000000 --- a/app/assets/javascripts/species-color-picker.js +++ /dev/null @@ -1,28 +0,0 @@ -/** - * SpeciesColorPicker web component - * - * Progressive enhancement for species/color picker forms: - * - Auto-submits the form when species or color changes (if JS is enabled) - * - Shows a submit button as fallback (if JS is disabled or slow to load) - * - Uses Custom Element internals API to communicate state to CSS - */ -class SpeciesColorPicker extends HTMLElement { - #internals; - - constructor() { - super(); - this.#internals = this.attachInternals(); - } - - connectedCallback() { - // Listen for changes to auto-submit the form, then tell CSS about it! - this.addEventListener("change", this.#handleChange); - this.#internals.states.add("auto-loading"); - } - - #handleChange(e) { - this.querySelector("form").requestSubmit(); - } -} - -customElements.define("species-color-picker", SpeciesColorPicker); diff --git a/app/assets/stylesheets/items/show.sass b/app/assets/stylesheets/items/show.sass index 968c19e8..2e1b6789 100644 --- a/app/assets/stylesheets/items/show.sass +++ b/app/assets/stylesheets/items/show.sass @@ -109,7 +109,7 @@ outfit-viewer .error-indicator display: block -species-color-picker +.species-color-picker .error-icon cursor: help margin-right: .25em @@ -130,7 +130,7 @@ species-color-picker animation-delay: .75s // Once the auto-loading behavior is ready, remove the submit button. - &:state(auto-loading) + auto-submit-form:state(auto-loading) input[type=submit] display: none @@ -296,7 +296,7 @@ species-face-picker width: 380px height: 380px - species-color-picker + .species-color-picker grid-area: picker species-face-picker diff --git a/app/assets/stylesheets/wardrobe/show.css b/app/assets/stylesheets/wardrobe/show.css index b215ff09..4b828adb 100644 --- a/app/assets/stylesheets/wardrobe/show.css +++ b/app/assets/stylesheets/wardrobe/show.css @@ -282,7 +282,7 @@ select, } } -:is(species-color-picker, pose-picker-popover):state(auto-loading) .progressive-submit { +auto-submit-form:state(auto-loading) .progressive-submit { display: none; } @@ -425,9 +425,10 @@ body.wardrobe-v2 { } /* Species/color picker */ - species-color-picker { + .species-color-picker { display: contents; + auto-submit-form, form { display: contents; } diff --git a/app/views/items/show.html.haml b/app/views/items/show.html.haml index a9db4e3c..524ec94b 100644 --- a/app/views/items/show.html.haml +++ b/app/views/items/show.html.haml @@ -33,20 +33,21 @@ Customize more = edit_icon - %species-color-picker - = form_for item_path(@item), method: :get, data: {"is-valid": @preview_error.nil?} do |f| - - if @preview_error == :pet_type_does_not_exist - %span.error-icon{title: "We haven't seen this kind of pet before."} ⚠️ - - elsif @preview_error == :no_item_data - %span.error-icon{title: "We haven't seen this item on this pet before."} ⚠️ + .species-color-picker + %auto-submit-form + = form_for item_path(@item), method: :get, data: {"is-valid": @preview_error.nil?} do |f| + - if @preview_error == :pet_type_does_not_exist + %span.error-icon{title: "We haven't seen this kind of pet before."} ⚠️ + - elsif @preview_error == :no_item_data + %span.error-icon{title: "We haven't seen this item on this pet before."} ⚠️ - = select_tag "preview[color_id]", - options_from_collection_for_select(Color.alphabetical, - "id", "human_name", @selected_preview_pet_type.color_id) - = select_tag "preview[species_id]", - options_from_collection_for_select(Species.alphabetical, - "id", "human_name", @selected_preview_pet_type.species_id) - = submit_tag "Go", name: nil + = select_tag "preview[color_id]", + options_from_collection_for_select(Color.alphabetical, + "id", "human_name", @selected_preview_pet_type.color_id) + = select_tag "preview[species_id]", + options_from_collection_for_select(Species.alphabetical, + "id", "human_name", @selected_preview_pet_type.species_id) + = submit_tag "Go", name: nil %species-face-picker %noscript @@ -138,5 +139,5 @@ - content_for :javascripts do = javascript_include_tag "idiomorph", async: true = javascript_include_tag "outfit-viewer", async: true - = javascript_include_tag "species-color-picker", async: true + = javascript_include_tag "auto-submit-form", async: true = javascript_include_tag "items/show", async: true diff --git a/app/views/wardrobe/appearance/_pose_picker.html.haml b/app/views/wardrobe/appearance/_pose_picker.html.haml index aad0ecad..bd32e31a 100644 --- a/app/views/wardrobe/appearance/_pose_picker.html.haml +++ b/app/views/wardrobe/appearance/_pose_picker.html.haml @@ -8,9 +8,11 @@ - active_tab = @alt_style ? "styles" : "expressions" %tab-panel{active: active_tab} .tab-content{"data-tab": "expressions", hidden: active_tab != "expressions" ? true : nil} - = render "appearance/pose_picker_form" + %auto-submit-form + = render "appearance/pose_picker_form" .tab-content{"data-tab": "styles", hidden: active_tab != "styles" ? true : nil} - = render "appearance/style_picker_form" + %auto-submit-form + = render "appearance/style_picker_form" .tab-list %button.tab-button{"data-tab": "expressions", type: "button", class: ("active" if active_tab == "expressions")} diff --git a/app/views/wardrobe/appearance/_species_color_picker.html.haml b/app/views/wardrobe/appearance/_species_color_picker.html.haml index 3c879d70..814c1a49 100644 --- a/app/views/wardrobe/appearance/_species_color_picker.html.haml +++ b/app/views/wardrobe/appearance/_species_color_picker.html.haml @@ -1,12 +1,13 @@ -%species-color-picker - = form_with url: @wardrobe_path, method: :get do |f| - = outfit_state_params except: [:color, :species] - = select_tag :color, - options_from_collection_for_select(@colors, "id", "human_name", - @selected_color&.id), - "aria-label": "Pet color" - = select_tag :species, - options_from_collection_for_select(@species, "id", "human_name", - @selected_species&.id), - "aria-label": "Pet species" - = submit_tag "Go", name: nil, class: "progressive-submit" +.species-color-picker + %auto-submit-form + = form_with url: @wardrobe_path, method: :get do |f| + = outfit_state_params except: [:color, :species] + = select_tag :color, + options_from_collection_for_select(@colors, "id", "human_name", + @selected_color&.id), + "aria-label": "Pet color" + = select_tag :species, + options_from_collection_for_select(@species, "id", "human_name", + @selected_species&.id), + "aria-label": "Pet species" + = submit_tag "Go", name: nil, class: "progressive-submit" diff --git a/app/views/wardrobe/show.html.haml b/app/views/wardrobe/show.html.haml index 5624a7f6..32a76ea2 100644 --- a/app/views/wardrobe/show.html.haml +++ b/app/views/wardrobe/show.html.haml @@ -13,7 +13,7 @@ = javascript_include_tag "application", async: true = javascript_include_tag "idiomorph", async: true = javascript_include_tag "outfit-viewer", async: true - = javascript_include_tag "species-color-picker", async: true + = javascript_include_tag "auto-submit-form", async: true = javascript_include_tag "pose-picker", async: true = javascript_include_tag "tab-panel", async: true = javascript_include_tag "outfit-rename-field", async: true diff --git a/docs/wardrobe-v2-migration.md b/docs/wardrobe-v2-migration.md index b48d6f88..0ebcb419 100644 --- a/docs/wardrobe-v2-migration.md +++ b/docs/wardrobe-v2-migration.md @@ -35,7 +35,7 @@ Code lives in `app/controllers/wardrobe_controller.rb`, `app/views/wardrobe/`, ` **URL as single source of truth**: All outfit state lives in URL params (`species`, `color`, `pose`, `style`, `objects[]`, `q[...]`). Every interaction is a GET request that generates a new URL. No client-side state management. Browser back/forward work naturally. -**Server-side rendering + Web Components**: All HTML is generated server-side. Lightweight web components (``, ``, ``, ``) add interactivity without framework overhead. +**Server-side rendering + Web Components**: All HTML is generated server-side. Lightweight web components (``, ``, ``, ``) add interactivity without framework overhead. **Progressive enhancement**: Submit buttons appear when JS is slow/disabled. Web components enhance forms with auto-submit on change.