Skip to content

[4.0.0-beta6] No event after hx-disable re-enables element; simple focus() after request is no longer possible #3947

Description

@infogulch

In Four, hx-disable controls remain disabled throughout __issueRequest with no event fired after they are re-enabled. The issue-request sequence is roughly:

  • disable elements
  • htmx:before:request
  • await fetch
  • htmx:before:response
  • await response.text()
  • htmx:after:request
  • htmx:finally:request
  • re-enable disabled elements

htmx/src/htmx.js

Lines 579 to 662 in 85b8378

async __issueRequest(ctx) {
let elt = ctx.sourceElement
let syncStrategy = this.__determineSyncStrategy(elt);
let requestQueue = this.__getRequestQueue(elt);
if (!requestQueue.issue(ctx, syncStrategy)) return
ctx.status = "issuing"
let indicators = [];
let disableElements = [];
try {
// Handle confirmation
if (ctx.confirm) {
let confirmed = await new Promise(resolve => {
let detail = {ctx, issueRequest: () => resolve(true), dropRequest: () => resolve(false)};
if (this.__trigger(elt, "htmx:confirm", detail)) {
let js = this.__extractJavascriptContent(ctx.confirm);
resolve(js ? this.__executeJavaScript(elt, {ctx}, js, true) : window.confirm(ctx.confirm));
}
});
if (!confirmed) return;
}
// initialize timeout & indicators after confirmation
this.__initTimeout(ctx);
indicators = this.__showIndicators(elt);
disableElements = this.__disableElements(elt);
ctx.fetch ||= window.fetch.bind(window)
if (!this.__trigger(elt, "htmx:before:request", {ctx})) return;
let response = await ctx.fetch(ctx.request.action, ctx.request);
ctx.response = {
raw: response,
status: response.status,
headers: response.headers,
}
this.__extractHxHeaders(ctx);
if (!this.__trigger(elt, "htmx:before:response", {ctx})) return;
ctx.text = await response.text();
if (!this.__trigger(elt, "htmx:after:request", {ctx})) return;
if (ctx.response.status >= 400) {
this.__trigger(elt, "htmx:response:error", {ctx})
}
if(this.__handleHeadersAndMaybeReturnEarly(ctx)){
ctx.keepIndicators = true;
return
}
if (ctx.status === "issuing") {
if (ctx.hx.retarget) ctx.target = ctx.hx.retarget; // HX-Retarget
if (ctx.hx.reswap) ctx.swap = ctx.hx.reswap; // HX-Reswap
if (ctx.hx.reselect) ctx.select = ctx.hx.reselect; // HX-Reselect
ctx.status = "response received";
this.__handleStatusCodes(ctx);
await this.swap(ctx);
ctx.status = "swapped";
}
} catch (error) {
ctx.status = "error: " + error;
this.__trigger(elt, "htmx:error", {ctx, error})
} finally {
clearTimeout(ctx.requestTimeout);
if (ctx.hx?.trigger) { // HX-Trigger
this.__handleTriggerHeader(ctx.hx.trigger, ctx.sourceElement);
}
this.__trigger(elt, "htmx:finally:request", {ctx})
if (!ctx.keepIndicators) {
this.__hideIndicators(indicators);
this.__enableElements(disableElements);
}
requestQueue.finish()
if (requestQueue.more()) {
// intentionally not awaited — __issueRequest has its own try/catch
this.__issueRequest(requestQueue.next())
}
}
}


hx-disable exists to prevent the user from interacting with the form while a request is in flight. One obvious thing one might want to do after the request is completed is re-focus the form element so the user can continue, say, typing in todo list items.

Without any event after the disabled status is removed, this kind of workflow becomes awkward to implement:

<form
    hx-disable="find input, find button"

    hx-on:htmx:after:request="find('form').reset(), find('find input').disabled = false, find('find input').focus()"
    <!-- or -->
    hx-on:htmx:after:request="find('form').reset(), setTimeout(() => find('find input').focus(), 0)"
    > 

It would be nice if there were some event that fired after the elements are re-enabled.

A few ideas:

  • Move enableElements call to before the htmx:finally:request event. This would be my preference; IMO "finally" should be fired after all default request behavior is completed.
  • Add a new event that fires after all default request behavior is completed. htmx:finallyfinally:request (joke)
  • Add hx-disable-specific event(s): htmx:{before,after}:{disable,reenable}. Maybe just htmx:after:reenable

Related issue: #3363

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions