bigskysoftware / htmx

</> htmx - high power tools for HTML
https://htmx.org
Other
38.19k stars 1.3k forks source link

hx-trigger="change" on a form does not react to input changes "outside" the form #2937

Open ouvreboite opened 1 month ago

ouvreboite commented 1 month ago

In HTML, it's possible to have form components (buttons, inputs, selects, ...) outside of a form by adding the form=#myFormId attribute on them.

<form id="myForm" hx-get...>
  <input type="text" name="text"/>
  <button type="submit">Submit</button>
</form>

<!-- I'm outside the form but still part of it -->
<select form="myForm" name="outside">
  <option>a</option>
  <option>b</option>
</select>

Using HTMX (via hx-boost or hx-get/hx-post) the outside select value is used correctly when making the call ✅ (ex: /currentPage?text=abc&outside=a)

But I can't find a way to trigger the form when the outside select value change.

<form id="myForm" hx-trigger="change" hx-get...>
  <input type="text" name="text"/> <!-- the form call will be triggered when this change -->
  <button type="submit">Submit</button>
</form>

<select form="myForm" name="outside" hx-trigger="change"> <!-- ... but not when this change -->
  <option>a</option>
  <option>b</option>
</select>

Example of the behavior: https://plnkr.co/edit/sXNvKYobVl6iD5NQ

A stopgap solution is to add a bit of JS on each out-of-form input: onchange="this.form.requestSubmit()" and have hx-trigger="submit change"

MichaelWest22 commented 1 month ago

Yeah the way form="xxx" and also hx-include work for adding remote inputs into a forms response do not propagate all of the various events from the remote objects back to the form. events like changed only bubble up to parents and this is just how events work in browsers sorry. Their are some exceptions like for buttons with form= set i think as these fire submit events to the remote form but this is all browser logic and not htmx.

I think the best solution is adding simple JS to the remote input to trigger the form which is what you have done. It is possible just with htmx to move the hx-get on the form up to a higher parent object to capture both the form and the selects change event and fire the request properly like this:

<body hx-get="item.html"
        hx-trigger="change"
        hx-target="#results"
        hx-swap="afterend"
        hx-include="#myForm">
    <section>
      <h1>Form</h1>
      <small>Selecting a value will trigger HTMX</small>
      <form
        id="myForm"
        "
      >
        <label>
          selectInForm
          <select name="selectInForm">
            <option>a</option>
            <option>b</option>
            <option>c</option>
          </select>
        </label>
      </form>
    </section>
    <section>
      <h1>Outside the form</h1>
      <small>HTMX is unable to react to change</small>
      <label>
        selectOutForm
        <select name="selectOutForm" form="myForm">
          <option>1</option>
          <option>2</option>
          <option>3</option>
        </select>
      </label>
    </section>
  </body>

This is not ideal on the body in this example but it shows how the events bubble up to parents and how you can use this sometimes.

You can also extend the attributes on the select to make it fire whatever event you like with htmx like this:

        <select name="selectOutForm" form="myForm" 
          hx-get="item.html"
          hx-trigger="change"
          hx-target="#results"
          hx-swap="afterend"
          hx-include="#myForm">
          <option>1</option>
          <option>2</option>
          <option>3</option>
        </select>

and then this select will function as its own customized htmx activated element. Also the hx-target, hx-swap and hx-include can all use inheritance so could be placed on a mutual parent element to reduce the attribute duplication if possible.

Telroshan commented 4 weeks ago

Note that you can also use the from modifier of hx-trigger

For example, defining

<form id="myForm" hx-post="/whatever" hx-trigger="change, change from:[form='myForm']">
<input ...>
...
</form>
<input form="myForm" ...>

will catch the change event on the form itself (thus on any contained child), but also from any input that defines the attribute form="myForm"

Note that, as the doc mentions about from:

The CSS selector is only evaluated once and is not re-evaluated when the page changes. If you need to detect dynamically added elements use an event filter, for example click[event.target.matches('input')]

If you need a form that handles dynamically added external inputs, you could listen on the body instead and filter the triggering elements, something like

<form id="myForm" hx-post="/whatever" hx-trigger="change, change[event.target.matches('[form=\'myForm\']')] from:body">
<input ...>
...
</form>
<input form="myForm" ...>

You can try it out on this JSFiddle