bigskysoftware / htmx-extensions

167 stars 51 forks source link

response-targets: add support for hx-swap-... overrides #87

Open volfpeter opened 3 months ago

volfpeter commented 3 months ago

Hi guys,

First of all, thanks for the great work on HTMX and its extensions. I just started a new project with it, and I must say I'm surprised how convenient it is to work with.

I ran into one issue though with the response-targets extension: it would be really handy if it was possible to override the hx-swap attribute similarly to hx-target, because the two are tightly related.

In my specific case, I'd need to replace a delete swap rule with innerHTML. I know I could tweak my components and API to be able to use the same swap rule, but that would really degrade the code (especially the API). My current workaround is to include a custom header in the HX request that triggers a custom "reswap" logic (HX-Reswap response header) on the backend, effectively implementing hx-swap-<error-code> manually on the backend, but solving this problem with an hx-swap-<error-code> attribute in the HTML would be way cleaner and more elegant in my opinion.

Telroshan commented 3 months ago

Good idea imo! If you'd like to work on a PR and suggest an implementation, feel free to do so!

volfpeter commented 3 months ago

I'm not familiar with the internals of HTMX yet, so it would take time for me to learn the necessary things (e.g. what to override in the event details) and implement (and test) this feature (I guess the pattern would be the same as for the target override). I would be happy to contribute this feature, but I can not promise for sure as my time is quite limited for the coming months. I'll leave a comment here if I can start work on this.

RizkyChandra commented 2 months ago

Made some small change 😄 tested on my project.

var swapPrefix = "hx-swap-";

// It's the same as getRespCodeTarget but the return value is the swap style string
function getSwapStyle(elt, respCodeNumber, prefix) {
  if (!elt || !respCodeNumber) return null;

  var respCode = respCodeNumber.toString();
  var attrPossibilities = [
    respCode,

    respCode.substr(0, 2) + "*",
    respCode.substr(0, 2) + "x",

    respCode.substr(0, 1) + "*",
    respCode.substr(0, 1) + "x",
    respCode.substr(0, 1) + "**",
    respCode.substr(0, 1) + "xx",

    "*",
    "x",
    "***",
    "xxx",
  ];
  if (startsWith(respCode, "4") || startsWith(respCode, "5")) {
    attrPossibilities.push("error");
  }

  for (var i = 0; i < attrPossibilities.length; i++) {
    var attr = prefix + attrPossibilities[i];
    var attrValue = api.getClosestAttributeValue(elt, attr);

    if (attrValue) {
      return attrValue;
    }

    // Uncomment this for more specific swap styles
    // if (
    //   attrValue == "innerHTML" ||
    //   attrValue == "outerHTML" ||
    //   attrValue == "beforebegin" ||
    //   attrValue == "afterbegin" ||
    //   attrValue == "beforeend" ||
    //   attrValue == "afterend" ||
    //   attrValue == "delete" ||
    //   attrValue == "none" ||
    //   attrValue == string
    // ) {
    //   return attrValue;
    // }
  }

  return null;
}

// Get the swap style inside the onEvent callback
var swapMethod = getSwapStyle(
  evt.detail.requestConfig.elt,
  evt.detail.xhr.status,
  swapPrefix
);
// Add the found swap style to the event detail
if (swapMethod) {
  evt.detail.swapStyle = swapMethod;
}
// If the swap style is not found, the default swap style from config will be used
Full Source Code ```js (function () { /** @type {import("../htmx").HtmxInternalApi} */ var api; var attrPrefix = "hx-target-"; var swapPrefix = "hx-swap-"; // IE11 doesn't support string.startsWith function startsWith(str, prefix) { return str.substring(0, prefix.length) === prefix; } /** * @param {HTMLElement} elt * @param {number} respCode * @returns {HTMLElement | null} */ function getRespCodeTarget(elt, respCodeNumber) { if (!elt || !respCodeNumber) return null; var respCode = respCodeNumber.toString(); // '*' is the original syntax, as the obvious character for a wildcard. // The 'x' alternative was added for maximum compatibility with HTML // templating engines, due to ambiguity around which characters are // supported in HTML attributes. // // Start with the most specific possible attribute and generalize from // there. var attrPossibilities = [ respCode, respCode.substr(0, 2) + "*", respCode.substr(0, 2) + "x", respCode.substr(0, 1) + "*", respCode.substr(0, 1) + "x", respCode.substr(0, 1) + "**", respCode.substr(0, 1) + "xx", "*", "x", "***", "xxx", ]; if (startsWith(respCode, "4") || startsWith(respCode, "5")) { attrPossibilities.push("error"); } for (var i = 0; i < attrPossibilities.length; i++) { var attr = attrPrefix + attrPossibilities[i]; var attrValue = api.getClosestAttributeValue(elt, attr); if (attrValue) { if (attrValue === "this") { return api.findThisElement(elt, attr); } else { return api.querySelectorExt(elt, attrValue); } } } return null; } /** * @param {HTMLElement} elt * @param {number} respCode * @returns {HTMLElement | null} */ function getSwapStyle(elt, respCodeNumber, prefix) { if (!elt || !respCodeNumber) return null; var respCode = respCodeNumber.toString(); var attrPossibilities = [ respCode, respCode.substr(0, 2) + "*", respCode.substr(0, 2) + "x", respCode.substr(0, 1) + "*", respCode.substr(0, 1) + "x", respCode.substr(0, 1) + "**", respCode.substr(0, 1) + "xx", "*", "x", "***", "xxx", ]; if (startsWith(respCode, "4") || startsWith(respCode, "5")) { attrPossibilities.push("error"); } for (var i = 0; i < attrPossibilities.length; i++) { var attr = prefix + attrPossibilities[i]; var attrValue = api.getClosestAttributeValue(elt, attr); if (attrValue) { return attrValue; } // Uncomment this for more specific swap styles // if ( // attrValue == "innerHTML" || // attrValue == "outerHTML" || // attrValue == "beforebegin" || // attrValue == "afterbegin" || // attrValue == "beforeend" || // attrValue == "afterend" || // attrValue == "delete" || // attrValue == "none" || // attrValue == string // ) { // return attrValue; // } } return null; } /** @param {Event} evt */ function handleErrorFlag(evt) { if (evt.detail.isError) { if (htmx.config.responseTargetUnsetsError) { evt.detail.isError = false; } } else if (htmx.config.responseTargetSetsError) { evt.detail.isError = true; } } htmx.defineExtension("response-targets", { /** @param {import("../htmx").HtmxInternalApi} apiRef */ init: function (apiRef) { api = apiRef; if (htmx.config.responseTargetUnsetsError === undefined) { htmx.config.responseTargetUnsetsError = true; } if (htmx.config.responseTargetSetsError === undefined) { htmx.config.responseTargetSetsError = false; } if (htmx.config.responseTargetPrefersExisting === undefined) { htmx.config.responseTargetPrefersExisting = false; } if (htmx.config.responseTargetPrefersRetargetHeader === undefined) { htmx.config.responseTargetPrefersRetargetHeader = true; } }, /** * @param {string} name * @param {Event} evt */ onEvent: function (name, evt) { if ( name === "htmx:beforeSwap" && evt.detail.xhr && evt.detail.xhr.status !== 200 ) { if (evt.detail.target) { if (htmx.config.responseTargetPrefersExisting) { evt.detail.shouldSwap = true; handleErrorFlag(evt); return true; } if ( htmx.config.responseTargetPrefersRetargetHeader && evt.detail.xhr.getAllResponseHeaders().match(/HX-Retarget:/i) ) { evt.detail.shouldSwap = true; handleErrorFlag(evt); return true; } } if (!evt.detail.requestConfig) { return true; } var target = getRespCodeTarget( evt.detail.requestConfig.elt, evt.detail.xhr.status ); if (target) { handleErrorFlag(evt); evt.detail.shouldSwap = true; evt.detail.target = target; } var swapMethod = getSwapStyle( evt.detail.requestConfig.elt, evt.detail.xhr.status, swapPrefix ); if (swapMethod) { evt.detail.swapStyle = swapMethod; } return true; } }, }); })(); ```