Repository: wagtail/wagtail

Issue:
🎛️ Migrate site switcher to use Stimulus approach `ActionController`
> ℹ️ **Part of the [Stimulus  🎛️ RFC 78](https://github.com/wagtail/rfcs/pull/78)**

### Is your proposal related to a problem?

There is a custom JavaScript implementation to add behaviour to select drop-down that will update the location (URL) when changed.

This approach should be very close to what we are already doing with the `SubmitController` so let's do a a bit of clean up to avoid too much ad-hoc JS.

### Describe the solution you'd like

* Update the implementation of `client/src/controllers/SubmitController.ts` to allow for a new [Stimulus Value](https://stimulus.hotwired.dev/reference/values) called `updateAction`.
  * When in use, the existing method `submit` will update the form's action value before submitting from the source element's value. `form.setAttribute('action', this.element.value); // example`
  * Essentially we want to use the form `get` submit to do the location change, instead of updating the `window.location.url`.
  * However, we need to ensure the right page is loaded, hence we need to revise `action` dynamically when the user selects the option.
* Remove the jQuery implementation completely [`wagtail/contrib/settings/static_src/wagtailsettings/js/site-switcher.js`](https://github.com/wagtail/wagtail/blob/main/wagtail/contrib/settings/static_src/wagtailsettings/js/site-switcher.js)
* Update the select field to have the suitable data attributes [`wagtail/contrib/settings/forms.py`](https://github.com/wagtail/wagtail/blob/main/wagtail/contrib/settings/forms.py#L23).
* Unit tests in JavaScript **must** be included with a PR.
* Validate that the 'current' option in the select drop-down for the site switcher is still function, so that selecting it will not do anything. See wagtail/contrib/settings/forms.py (Update: This is not a huge problem, the browser will not trigger a `change` event if the value has not changed).

#### Example HTML

```html
<form method="get" id="settings-site-switch" novalidate>
    <select
        name="site-switcher"
        data-controller="w-submit"
        data-action="change->w-submit#submit"
        data-w-submit-update-action-value="true"
    >
        <option value="/path/to/current-site" selected>current.com</option>
        <option value="/path/to/other-site">other.com</option>
    </select>
</form>
```


### Additional notes

* Remember that Site Settings is not available in the bakery demo by default, you will need to add this locally to validate the behaviour https://docs.wagtail.org/en/stable/reference/contrib/settings.html
* `AutoFieldController` was added in this PR https://github.com/wagtail/wagtail/pull/9337 and then renamed to `SubmitController` in https://github.com/wagtail/wagtail/pull/10098
* The actual `form` HTML is located in [`wagtail/contrib/settings/templates/wagtailsettings/edit.html`](https://github.com/wagtail/wagtail/blob/main/wagtail/contrib/settings/templates/wagtailsettings/edit.html) - this HTML should not need changes but good to note



Discussion:
Flagging as a good first issue, however, whoever does this will need to read up on Stimulus to get an understanding of the code.

@Lovelyfin00  - do you mind reviewing this issue and seeing if we should add any links/notes to make it easier to pick up. This may be a great chance for you to assist someone contributing if anyone picks this up. This will also be a good chance for you to have a think about how we document Stimulus for developers that make contributions to Wagtail.


Resolving patch:
diff --git a/wagtail/contrib/settings/forms.py b/wagtail/contrib/settings/forms.py
--- a/wagtail/contrib/settings/forms.py
+++ b/wagtail/contrib/settings/forms.py
@@ -2,20 +2,19 @@
 from django.urls import reverse
 from django.utils.translation import gettext_lazy as _
 
-from wagtail.admin.staticfiles import versioned_static
 from wagtail.models import Site
 
 
 class SiteSwitchForm(forms.Form):
-    site = forms.ChoiceField(choices=[])
-
-    @property
-    def media(self):
-        return forms.Media(
-            js=[
-                versioned_static("wagtailsettings/js/site-switcher.js"),
-            ]
-        )
+    site = forms.ChoiceField(
+        choices=[],
+        widget=forms.Select(
+            attrs={
+                "data-controller": "w-action",
+                "data-action": "change->w-action#redirect",
+            }
+        ),
+    )
 
     def __init__(self, current_site, model, **kwargs):
         initial_data = {"site": self.get_change_url(current_site, model)}


Test patch:
diff --git a/client/src/controllers/ActionController.test.js b/client/src/controllers/ActionController.test.js
--- a/client/src/controllers/ActionController.test.js
+++ b/client/src/controllers/ActionController.test.js
@@ -3,9 +3,23 @@ import { ActionController } from './ActionController';
 
 describe('ActionController', () => {
   let app;
+  const oldWindowLocation = window.location;
+
+  beforeAll(() => {
+    delete window.location;
+
+    window.location = Object.defineProperties(
+      {},
+      {
+        ...Object.getOwnPropertyDescriptors(oldWindowLocation),
+        assign: { configurable: true, value: jest.fn() },
+      },
+    );
+  });
 
   afterEach(() => {
     app?.stop();
+    jest.clearAllMocks();
   });
 
   describe('post method', () => {
@@ -70,4 +84,62 @@ describe('ActionController', () => {
       expect(clickMock).toHaveBeenCalled();
     });
   });
+
+  describe('redirect method', () => {
+    beforeEach(() => {
+      document.body.innerHTML = `
+      <select name="url" data-controller="w-action" data-action="change->w-action#redirect">
+        <option value="http://localhost/place?option=1">1</option>
+        <option value="http://localhost/place?option=2" selected>2</option>
+      </select>
+      `;
+
+      app = Application.start();
+      app.register('w-action', ActionController);
+    });
+
+    it('should have a redirect method that falls back to any element value', () => {
+      const select = document.querySelector('select');
+
+      expect(window.location.href).toEqual('http://localhost/');
+      expect(window.location.assign).not.toHaveBeenCalled();
+
+      select.dispatchEvent(new CustomEvent('change'));
+
+      expect(window.location.assign).toHaveBeenCalledWith(
+        'http://localhost/place?option=2',
+      );
+    });
+
+    it('should allow redirection via the custom event detail', () => {
+      const select = document.querySelector('select');
+
+      expect(window.location.href).toEqual('http://localhost/');
+      expect(window.location.assign).not.toHaveBeenCalled();
+
+      select.dispatchEvent(
+        new CustomEvent('change', { detail: { url: '/its/in/the/detail/' } }),
+      );
+
+      expect(window.location.assign).toHaveBeenCalledWith(
+        '/its/in/the/detail/',
+      );
+    });
+
+    it('should allow redirection via the Stimulus param approach', () => {
+      const select = document.querySelector('select');
+
+      expect(window.location.href).toEqual('http://localhost/');
+      expect(window.location.assign).not.toHaveBeenCalled();
+
+      select.dataset.wActionUrlParam = '/check/out/the/param/';
+
+      select.dispatchEvent(
+        new CustomEvent('change', { detail: { url: '/its/in/the/detail/' } }),
+      );
+      expect(window.location.assign).toHaveBeenCalledWith(
+        '/check/out/the/param/',
+      );
+    });
+  });
 });