Repository: wagtail/wagtail

Issue:
🎛️ Migrate header search Stimulus Controller
> ℹ️ **Part of the [Stimulus  🎛️ RFC 78](https://github.com/wagtail/rfcs/pull/78)**

### Is your proposal related to a problem?

We have a core.js implementations of JavaScript code that, when a matching search input receives changes, will trigger an async request to the relevant search results listing. Once the endpoint returns with HTML, it will be patched into the results container HTML element.

### Describe the solution you'd like

* Create a stimulus controller `w-search` that will replace the existing ad-hoc JS implementation
* The behaviour should be exactly the same as current state but using Stimulus data attributes for the behaviour & classes declaration (note: likely we will drop the `autofocus` and may not re-introduce the `slide` jQuery animation)
* Controller should be written in TypeScript
* Ensure that the existing unit tests are created to reflect this new behaviour
* We will need to document an upgrade consideration that the previous `window.headerSearch` approach will not work in a future release.
* We may want to introduce a console warning once all the Wagtail usage of `window.headerSearch` has been removed
* Nice to have - a Storybook story for this component

### Additional context

* Implementation https://github.com/wagtail/wagtail/blob/main/client/src/entrypoints/admin/core.js#L251-L306
* There is a very similar (almost cut & paste) of logic that is used in the chooser modals for searching here https://github.com/wagtail/wagtail/blob/main/client/src/includes/chooserModal.js#L109-L176 (the Stimulus will likely replace this but may be out of scope for this issue

### Potential approach

#### Support `input` only usage (with using `window.headerSearch` config)

```JS
window.headerSearch = {
    url: "{% url 'wagtailimages:listing_results' %}",
    targetOutput: "#image-results"
}
```

```html
<div class="w-field__input" data-field-input="">
    <svg class="icon icon-search w-field__icon" aria-hidden="true">
        <use href="#icon-search"></use>
    </svg>
    <input
        type="text"
        name="q"
        placeholder="Search images"
        data-controller="w-search"
        data-action="change->w-search#search cut->w-search#search keyup->w-search#search paste->w-search#search"
        id="id_q"
    />
</div>
```

#### Support `input` only usage

```html
<div class="w-field__input" data-field-input="">
    <svg class="icon icon-search w-field__icon" aria-hidden="true">
        <use href="#icon-search"></use>
    </svg>
    <input
        type="text"
        name="q"
        placeholder="Search images"
        data-controller="w-search"
        data-action="change->w-search#search cut->w-search#search keyup->w-search#search paste->w-search#search"
        id="id_q"
        data-w-search-results-value="#image-results"
        data-w-search-url-value="/admin/images/results/"
    />
</div>
```

#### Support controlled form with search input as a target

```html
<form
  class="col search-form"
  action="/admin/images/"
  method="get"
  novalidate=""
  role="search"
  data-controller="w-search"
  data-w-search-url-value="/admin/images/results/"
>
  <div class="w-field__wrapper w-mb-0" data-field-wrapper="">
    <label class="w-field__label w-sr-only" for="id_q" id="id_q-label">Search term</label>
    <div class="w-field w-field--char_field w-field--text_input">
      <div class="w-field__input" data-field-input="">
        <svg class="icon icon-search w-field__icon" aria-hidden="true"><use href="#icon-search"></use></svg>
        <input
          type="text"
          name="q"
          placeholder="Search images"
          data-w-search-target="input"
          data-action="change->w-search#search cut->w-search#search keyup->w-search#search paste->w-search#search"
          id="id_q"
        />
      </div>
    </div>
  </div>
  <div class="visuallyhidden"><input disabled="" type="submit" aria-hidden="true" /></div>
</form>
```


Discussion:
Initial implementation is in for this, will aim to do a follow up PR that pulls out the inline scripts that we should no longer need.

Resolving patch:
diff --git a/wagtail/admin/forms/search.py b/wagtail/admin/forms/search.py
--- a/wagtail/admin/forms/search.py
+++ b/wagtail/admin/forms/search.py
@@ -7,7 +7,10 @@ class SearchForm(forms.Form):
     def __init__(self, *args, **kwargs):
         placeholder = kwargs.pop("placeholder", _("Search"))
         super().__init__(*args, **kwargs)
-        self.fields["q"].widget.attrs = {"placeholder": placeholder}
+        self.fields["q"].widget.attrs = {
+            "placeholder": placeholder,
+            "data-w-swap-target": "input",
+        }
 
     q = forms.CharField(
         label=gettext_lazy("Search term"),


Test patch:
diff --git a/client/src/controllers/SwapController.test.js b/client/src/controllers/SwapController.test.js
--- a/client/src/controllers/SwapController.test.js
+++ b/client/src/controllers/SwapController.test.js
@@ -43,7 +43,7 @@ describe('SwapController', () => {
       expect(handleError).not.toHaveBeenCalled();
 
       document.body.innerHTML = `
-      <div id="results"></div>
+      <div id="listing-results"></div>
       <input
         id="search"
         type="text"
@@ -66,7 +66,7 @@ describe('SwapController', () => {
       expect(handleError).not.toHaveBeenCalled();
 
       document.body.innerHTML = `
-      <div id="results"></div>
+      <div id="listing-results"></div>
       <input
         id="search"
         type="text"
@@ -112,7 +112,7 @@ describe('SwapController', () => {
     });
   });
 
-  describe('fallback on window.headerSearch values if not in HTML', () => {
+  describe('fallback on window.headerSearch values if not in HTML (RemovedInWagtail60)', () => {
     it('should set the src & target value from the window.headerSearch if not present', async () => {
       window.headerSearch = {
         termInput: '#search',