idiomorph icon indicating copy to clipboard operation
idiomorph copied to clipboard

Idiomorph explicitly set swap strategy is overridden by inherited htmx swap, and disinheriting does not change the behavior.

Open ranzlee opened this issue 9 months ago • 3 comments

This is a change validation feature where a debounced input change issues a request. The intent is to morph the changed field with a validation error if invalid. I did work around this by changing the var source to be 'my-div' and moving the div inside the form. I think the issue is that htmx.ajax was using the form as the source and the swap strategy was locked-in. Explicitly setting idiomorph to swap outerHTML has no effect, it still applies the form swap strategy. I'm not sure if this is an idiomorph or htmx bug, or combination. As I mentioned, I was able to design my way around it, but wanted to report it just in case. P.S. I love htmx and idiomorph! Quality libraries and much appreciated! Thank you!

Here is the commit of the work-around, if it helps.

versions: htmx: 2.0.4 idiomorph: 0.7.3

<div id="my-div" hx-ext="morph" >
  <form id="my-form" hx-post="/post-route" hx-swap="beforeend" hx-target="#some-list">
     <!--  content -->
      <script>
          (function() {
              var validate = function(evt) {
                  evt.stopPropagation();
                  if (!evt.srcElement.hasAttribute("data-validate-on-change")) {
                      return;
                  }
                  var dataId = evt.srcElement.getAttribute("data-id");
                  var source = document.getElementById('my-form');
                  htmx.ajax('POST', '/validation-route', {
                      select: `#${dataId}`,
                      target: `#${dataId}`,
                      swap: "morph:{morphStyle:'outerHTML', ignoreActiveValue:true}",
                      source: source
                  }).then(() => {
                      var evt = new Event('validationChange');
                      var ele = document.getElementById(dataId);
                      ele?.dispatchEvent(evt);
                  });
              }
              var debounce = function() {
                  var timer;
                  return function(evt) {
                      clearTimeout(timer);
                      timer = setTimeout(() => {
                          validate(evt);
                      }, 600);
                  }
              }
              document.getElementById('my-div').oninput = debounce(); 
          })()
      </script>
    </form>
  </div>

ranzlee avatar Apr 07 '25 22:04 ranzlee

swap styles in htmx use space as a separator so you should never have space anywhere unless you are using the advanced swap modifiers listed here https://htmx.org/attributes/hx-swap/

Try changing it to

swap: "morph:{morphStyle:'outerHTML',ignoreActiveValue:true}",

MichaelWest22 avatar Apr 08 '25 01:04 MichaelWest22

Also I think this feature you have implemented as a custom JS event handler can actually all be implemented using htmx core attributes like:

<form>
 <div id="someid" 
    hx-post="/validation-route"
    hx-select="#someid"
    hx-swap="morph:{morphStyle:'outerHTML',ignoreActiveValue:true}"
    hx-trigger="input changed delay:600ms">
    <input name="somename">
    <p>some validation text i may want to update</p>
  </div>
</form>

You can wrap this kind of div around each input one by one or just have one div wrapping all the inputs and morph the validation for all inputs each time one of them changes as input events bubble up from each input to all parent div's

MichaelWest22 avatar Apr 08 '25 03:04 MichaelWest22

Thank you for your response, Michael!

I tried to recreate the code example by hand, but I did remove the spaces and the htmx swap is prioritized ahead of the idiomorph swap strategy.

As for the alternate solution, I agree. This is more of a situation where the user (developer) doesn't need to know the implementation details, hence the htmx.ajax usage. I only use it for the validator component (this) and for file uploads where I need to track progress. If this were code I was writing I would use the attributes as you suggest. That said, targeting the container div instead of the form that contains the triggering element is the solution and works great! I just thought I should report it as I do believe it is a bug, albeit minor and probably not going to be tripped over unless someone is doing something crazy like me :)

My use case is in a ASP.NET RazorComponent where the intent is something like this...

    <RxChangeValidator
        Id="todos-validator"
        ValidationPostRoute="/todos/validate"
        IsDisabled="@(false)">
        <input type="hidden" name="@(nameof(Model.M.Id))" value="@(Model.M.Id)">
        <div>
            <!-- Title Field -->
            <Field
                Id="@($"{nameof(Model.M.Title)}{Model.M.Id}")"
                PropertyName="@(nameof(Model.M.Title))"
                Value="@(Model.M.Title)"
                Label="Title"
                InputType="text"
                UseOpacityForValidationErrors="@(true)"
                AllowValidateOnChange="@(true)"
                maxlength="80"
                placeholder="e.g., Learn the RazorX meta-framework!">
            </Field>
        </div>
        <div>
            <!-- Description Memo Field -->
            <MemoField
                Id="@($"{nameof(Model.M.Description)}{Model.M.Id}")"
                PropertyName="@(nameof(Model.M.Description))"
                Value="@(Model.M.Description)"
                Label="Description"
                MaxLength="500"
                UseOpacityForValidationErrors="@(true)"
                AllowValidateOnChange="@(true)"
                placeholder="e.g., This includes reading the htmx documentation and checking out Tailwind and daisyUI.">
            </MemoField>
        </div>
    </RxChangeValidator>

ranzlee avatar Apr 08 '25 21:04 ranzlee