diff options
author | carson <carson@leaddyno.com> | 2020-06-12 13:42:55 -0700 |
---|---|---|
committer | carson <carson@leaddyno.com> | 2020-06-12 13:42:55 -0700 |
commit | 0ac641b63fc7b2d313cad03f37742c8922eef0a2 (patch) | |
tree | 16d902b83c17a611a8e8e94509c32bbe4ef052e8 | |
parent | 7a0010f43cb84b251d0d184d8c4a54480e795570 (diff) | |
download | htmx-0ac641b63fc7b2d313cad03f37742c8922eef0a2.tar.gz htmx-0ac641b63fc7b2d313cad03f37742c8922eef0a2.zip |
remove the hx-error-url attribute in favor of hyperscript
add basic hyperscript compatibility testing
-rw-r--r-- | src/htmx.js | 21 | ||||
-rw-r--r-- | test/attributes/hx-error-url.js | 32 | ||||
-rw-r--r-- | test/ext/hyperscript.js | 37 | ||||
-rw-r--r-- | test/index.html | 8 | ||||
-rw-r--r-- | test/lib/_hyperscript.js | 1524 | ||||
-rw-r--r-- | www/attributes/hx-error-url.md | 24 | ||||
-rw-r--r-- | www/docs.md | 2 | ||||
-rw-r--r-- | www/reference.md | 1 |
8 files changed, 1577 insertions, 72 deletions
diff --git a/src/htmx.js b/src/htmx.js index fc53fd54..54761675 100644 --- a/src/htmx.js +++ b/src/htmx.js @@ -888,11 +888,19 @@ return (function () { }); } + function isHyperScriptAvailable() { + return typeof _hyperscript !== "undefined"; + } + function processNode(elt) { var nodeData = getInternalData(elt); if (!nodeData.processed) { nodeData.processed = true; + if(isHyperScriptAvailable()){ + _hyperscript.init(elt); + } + if (elt.value) { nodeData.lastValue = elt.value; } @@ -914,7 +922,6 @@ return (function () { processWebSocketInfo(elt, nodeData, wsInfo); } triggerEvent(elt, "processedNode.htmx"); - } if (elt.children) { // IE forEach(elt.children, function(child) { processNode(child) }); @@ -925,16 +932,6 @@ return (function () { // Event/Log Support //==================================================================== - function sendError(elt, eventName, detail) { - var errorURL = getClosestAttributeValue(elt, "hx-error-url"); - if (errorURL) { - var xhr = new XMLHttpRequest(); - xhr.open("POST", errorURL); - xhr.setRequestHeader("Content-Type", "application/json;charset=UTF-8"); - xhr.send(JSON.stringify({ "elt": elt.id, "event": eventName, "detail" : detail })); - } - } - function makeEvent(eventName, detail) { var evt; if (window.CustomEvent && typeof window.CustomEvent === 'function') { @@ -983,7 +980,7 @@ return (function () { } if (detail.error) { logError(detail.error); - sendError(elt, eventName, detail); + triggerEvent(elt, "error.htmx", {errorDetail:detail}) } var eventResult = elt.dispatchEvent(event); withExtensions(elt, function (extension) { diff --git a/test/attributes/hx-error-url.js b/test/attributes/hx-error-url.js deleted file mode 100644 index 3ee46aa7..00000000 --- a/test/attributes/hx-error-url.js +++ /dev/null @@ -1,32 +0,0 @@ -describe("hx-error-url attribute", function(){ - beforeEach(function() { - this.server = makeServer(); - clearWorkArea(); - }); - afterEach(function() { - this.server.restore(); - clearWorkArea(); - }); - - it('Submits a POST with error content on bad request', function() - { - this.server.respondWith("POST", "/error", function(xhr){ - should.equal(JSON.parse(xhr.requestBody).detail.xhr.status, 404); - }); - var btn = make('<button hx-error-url="/error" hx-get="/bad">Click Me!</button>') - btn.click(); - this.server.respond(); - this.server.respond(); - }); - - it('Submits a POST with error content on bad request w/ data-* prefix', function() - { - this.server.respondWith("POST", "/error", function(xhr){ - should.equal(JSON.parse(xhr.requestBody).detail.xhr.status, 404); - }); - var btn = make('<button data-hx-error-url="/error" hx-get="/bad">Click Me!</button>') - btn.click(); - this.server.respond(); - this.server.respond(); - }); -}) diff --git a/test/ext/hyperscript.js b/test/ext/hyperscript.js new file mode 100644 index 00000000..37602c6e --- /dev/null +++ b/test/ext/hyperscript.js @@ -0,0 +1,37 @@ +describe("hyperscript integration", function() { + beforeEach(function () { + this.server = makeServer(); + clearWorkArea(); + }); + afterEach(function () { + this.server.restore(); + clearWorkArea(); + }); + + it('can trigger with a custom event', function () { + this.server.respondWith("GET", "/test", "Custom Event Sent!"); + var btn = make('<button _="on click send customEvent" hx-trigger="customEvent" hx-get="/test">Click Me!</button>') + btn.click(); + this.server.respond(); + btn.innerHTML.should.equal("Custom Event Sent!"); + }); + + it('can handle htmx driven events', function () { + this.server.respondWith("GET", "/test", "Clicked!"); + var btn = make('<button _="on afterSettle.htmx add .afterSettle" hx-get="/test">Click Me!</button>') + btn.classList.contains("afterSettle").should.equal(false); + btn.click(); + this.server.respond(); + btn.classList.contains("afterSettle").should.equal(true); + }); + + it('can handle htmx error events', function () { + this.server.respondWith("GET", "/test", [404, {}, "Bad request"]); + var div = make('<div id="d1"></div>') + var btn = make('<button _="on error.htmx put event.detail.errorDetail.error into #d1.innerHTML" hx-get="/test">Click Me!</button>') + btn.click(); + this.server.respond(); + div.innerHTML.should.equal("Response Status Error Code 404 from /test"); + }); + +});
\ No newline at end of file diff --git a/test/index.html b/test/index.html index 13fc2bd0..1b47a4dd 100644 --- a/test/index.html +++ b/test/index.html @@ -64,7 +64,6 @@ <!-- attribute tests --> <script src="attributes/hx-boost.js"></script> <script src="attributes/hx-delete.js"></script> -<script src="attributes/hx-error-url.js"></script> <script src="attributes/hx-ext.js"></script> <script src="attributes/hx-get.js"></script> <script src="attributes/hx-include.js"></script> @@ -82,6 +81,13 @@ <script src="attributes/hx-trigger.js"></script> <script src="attributes/hx-ws.js"></script> +<!-- hyperscript integration --> +<script src="lib/_hyperscript.js"></script> +<script src="ext/hyperscript.js"></script> +<script> + _hyperscript.start(); +</script> + <!-- extension tests --> <script src="../src/ext/method-override.js"></script> <script src="ext/method-override.js"></script> diff --git a/test/lib/_hyperscript.js b/test/lib/_hyperscript.js new file mode 100644 index 00000000..97697430 --- /dev/null +++ b/test/lib/_hyperscript.js @@ -0,0 +1,1524 @@ +//AMD insanity +(function (root, factory) { + if (typeof define === 'function' && define.amd) { + // AMD. Register as an anonymous module. + define([], factory); + } else { + // Browser globals + root._hyperscript = factory(); + } +}(typeof self !== 'undefined' ? self : this, function () { + return (function () { + 'use strict'; + + //----------------------------------------------- + // Lexer + //----------------------------------------------- + var _lexer = function () { + var OP_TABLE = { + '+': 'PLUS', + '-': 'MINUS', + '*': 'MULTIPLY', + '/': 'DIVIDE', + '.': 'PERIOD', + '\\': 'BACKSLASH', + ':': 'COLON', + '%': 'PERCENT', + '|': 'PIPE', + '!': 'EXCLAMATION', + '?': 'QUESTION', + '#': 'POUND', + '&': 'AMPERSAND', + ';': 'SEMI', + ',': 'COMMA', + '(': 'L_PAREN', + ')': 'R_PAREN', + '<': 'L_ANG', + '>': 'R_ANG', + '<=': 'LTE_ANG', + '>=': 'GTE_ANG', + '==': 'EQ', + '===': 'EQQ', + '{': 'L_BRACE', + '}': 'R_BRACE', + '[': 'L_BRACKET', + ']': 'R_BRACKET', + '=': 'EQUALS' + }; + + function isValidCSSClassChar(c) { + return isAlpha(c) || isNumeric(c) || c === "-" || c === "_"; + } + + function isValidCSSIDChar(c) { + return isAlpha(c) || isNumeric(c) || c === "-" || c === "_" || c === ":"; + } + + function isWhitespace(c) { + return c === " " || c === "\t" || isNewline(c); + } + + function positionString(token) { + return "[Line: " + token.line + ", Column: " + token.col + "]" + } + + function isNewline(c) { + return c === '\r' || c === '\n'; + } + + function isNumeric(c) { + return c >= '0' && c <= '9'; + } + + function isAlpha(c) { + return (c >= 'a' && c <= 'z') || + (c >= 'A' && c <= 'Z'); + } + + + function makeTokensObject(tokens, consumed, source) { + + function raiseError(tokens, error) { + _parser.raiseParseError(tokens, error); + } + + function requireOpToken(value) { + var token = matchOpToken(value); + if (token) { + return token; + } else { + raiseError(this, "Expected '" + value + "' but found '" + currentToken().value + "'"); + } + } + + function matchAnyOpToken(op1, op2, op3) { + for (var i = 0; i < arguments.length; i++) { + var opToken = arguments[i]; + var match = matchOpToken(opToken); + if (match) { + return match; + } + } + } + + function matchOpToken(value) { + if (currentToken() && currentToken().op && currentToken().value === value) { + return consumeToken(); + } + } + + function requireTokenType(type1, type2, type3, type4) { + var token = matchTokenType(type1, type2, type3, type4); + if (token) { + return token; + } else { + raiseError(this, "Expected one of " + JSON.stringify([type1, type2, type3])); + } + } + + function matchTokenType(type1, type2, type3, type4) { + if (currentToken() && currentToken().type && [type1, type2, type3, type4].indexOf(currentToken().type) >= 0) { + return consumeToken(); + } + } + + function requireToken(value, type) { + var token = matchToken(value, type); + if (token) { + return token; + } else { + raiseError(this, "Expected '" + value + "' but found '" + currentToken().value + "'"); + } + } + + function matchToken(value, type) { + var type = type || "IDENTIFIER"; + if (currentToken() && currentToken().value === value && currentToken().type === type) { + return consumeToken(); + } + } + + function consumeToken() { + var match = tokens.shift(); + consumed.push(match); + return match; + } + + function hasMore() { + return tokens.length > 0; + } + + function currentToken() { + return tokens[0]; + } + + return { + matchAnyOpToken: matchAnyOpToken, + matchOpToken: matchOpToken, + requireOpToken: requireOpToken, + matchTokenType: matchTokenType, + requireTokenType: requireTokenType, + consumeToken: consumeToken, + matchToken: matchToken, + requireToken: requireToken, + list: tokens, + source: source, + hasMore: hasMore, + currentToken: currentToken + } + } + + function tokenize(string) { + var source = string; + var tokens = []; + var position = 0; + var column = 0; + var line = 1; + var lastToken = "<START>"; + + while (position < source.length) { + consumeWhitespace(); + if (currentChar() === "-" && nextChar() === "-") { + consumeComment(); + } else { + if (!possiblePrecedingSymbol() && currentChar() === "." && isAlpha(nextChar())) { + tokens.push(consumeClassReference()); + } else if (!possiblePrecedingSymbol() && currentChar() === "#" && isAlpha(nextChar())) { + tokens.push(consumeIdReference()); + } else if (isAlpha(currentChar())) { + tokens.push(consumeIdentifier()); + } else if (isNumeric(currentChar())) { + tokens.push(consumeNumber()); + } else if (currentChar() === '"' || currentChar() === "'") { + tokens.push(consumeString()); + } else if (OP_TABLE[currentChar()]) { + tokens.push(consumeOp()); + } else { + if (position < source.length) { + throw Error("Unknown token: " + currentChar() + " "); + } + } + } + } + + return makeTokensObject(tokens, [], source); + + function makeOpToken(type, value) { + var token = makeToken(type, value); + token.op = true; + return token; + } + + function makeToken(type, value) { + return { + type: type, + value: value, + start: position, + end: position + 1, + column: column, + line: line + }; + } + + function consumeComment() { + while (currentChar() && !isNewline(currentChar())) { + consumeChar(); + } + consumeChar(); + } + + function consumeClassReference() { + var classRef = makeToken("CLASS_REF"); + var value = consumeChar(); + while (isValidCSSClassChar(currentChar())) { + value += consumeChar(); + } + classRef.value = value; + classRef.end = position; + return classRef; + } + + + function consumeIdReference() { + var idRef = makeToken("ID_REF"); + var value = consumeChar(); + while (isValidCSSIDChar(currentChar())) { + value += consumeChar(); + } + idRef.value = value; + idRef.end = position; + return idRef; + } + + function consumeIdentifier() { + var identifier = makeToken("IDENTIFIER"); + var value = consumeChar(); + while (isAlpha(currentChar())) { + value += consumeChar(); + } + identifier.value = value; + identifier.end = position; + return identifier; + } + + function consumeNumber() { + var number = makeToken("NUMBER"); + var value = consumeChar(); + while (isNumeric(currentChar())) { + value += consumeChar(); + } + if (currentChar() === ".") { + value += consumeChar(); + } + while (isNumeric(currentChar())) { + value += consumeChar(); + } + number.value = value; + number.end = position; + return number; + } + + function consumeOp() { + var value = consumeChar(); // consume leading char + while (currentChar() && OP_TABLE[value + currentChar()]) { + value += consumeChar(); + } + var op = makeOpToken(OP_TABLE[value], value); + op.value = value; + op.end = position; + return op; + } + + function consumeString() { + var string = makeToken("STRING"); + var startChar = consumeChar(); // consume leading quote + var value = ""; + while (currentChar() && currentChar() !== startChar) { + if (currentChar() === "\\") { + consumeChar(); // consume escape char and move on + } + value += consumeChar(); + } + if (currentChar() !== startChar) { + throw Error("Unterminated string at " + positionString(string)); + } else { + consumeChar(); // consume final quote + } + string.value = value; + string.end = position; + return string; + } + + function currentChar() { + return source.charAt(position); + } + + function nextChar() { + return source.charAt(position + 1); + } + + function consumeChar() { + lastToken = currentChar(); + position++; + column++; + return lastToken; + } + + function possiblePrecedingSymbol() { + return isAlpha(lastToken) || isNumeric(lastToken) || lastToken === ")" || lastToken === "}" || lastToken === "]" + } + + function consumeWhitespace() { + while (currentChar() && isWhitespace(currentChar())) { + if (isNewline(currentChar())) { + column = 0; + line++; + } + consumeChar(); + } + } + } + + return { + tokenize: tokenize + } + }(); + + //----------------------------------------------- + // Parser + //----------------------------------------------- + var _parser = function () { + + var GRAMMAR = {} + + function addGrammarElement(name, definition) { + GRAMMAR[name] = definition; + } + + function createParserContext(tokens) { + var currentToken = tokens.currentToken(); + var source = tokens.source; + var lines = source.split("\n"); + var line = currentToken ? currentToken.line - 1 : lines.length - 1; + var contextLine = lines[line]; + var offset = currentToken ? currentToken.column : contextLine.length - 1; + return contextLine + "\n" + " ".repeat(offset) + "^^\n\n"; + } + + function raiseParseError(tokens, message) { + message = (message || "Unexpected Token : " + tokens.currentToken().value) + "\n\n" + + createParserContext(tokens); + var error = new Error(message); + error.tokens = tokens; + throw error + } + + function parseElement(type, tokens, root) { + var expressionDef = GRAMMAR[type]; + if (expressionDef) return expressionDef(_parser, tokens, root); + } + + function parseAnyOf(types, tokens) { + for (var i = 0; i < types.length; i++) { + var type = types[i]; + var expression = parseElement(type, tokens); + if (expression) { + return expression; + } + } + } + + function parseHyperScript(tokens) { + return parseElement("hyperscript", tokens) + } + + function transpile(node, defaultVal) { + if (node == null) { + return defaultVal; + } + var src = node.transpile(); + if (node.next) { + return src + "\n" + transpile(node.next) + } else { + return src; + } + } + + return { + // parser API + parseElement: parseElement, + parseAnyOf: parseAnyOf, + parseHyperScript: parseHyperScript, + raiseParseError: raiseParseError, + addGrammarElement: addGrammarElement, + transpile: transpile + } + }(); + + //----------------------------------------------- + // Runtime + //----------------------------------------------- + var _runtime = function () { + var SCRIPT_ATTRIBUTES = ["_", "script", "data-script"]; + + function matchesSelector(elt, selector) { + // noinspection JSUnresolvedVariable + var matchesFunction = elt.matches || + elt.matchesSelector || elt.msMatchesSelector || elt.mozMatchesSelector + || elt.webkitMatchesSelector || elt.oMatchesSelector; + return matchesFunction && matchesFunction.call(elt, selector); + } + + function makeEvent(eventName, detail) { + var evt; + if (window.CustomEvent && typeof window.CustomEvent === 'function') { + evt = new CustomEvent(eventName, {bubbles: true, cancelable: true, detail: detail}); + } else { + evt = document.createEvent('CustomEvent'); + evt.initCustomEvent(eventName, true, true, detail); + } + return evt; + } + + function triggerEvent(elt, eventName, detail) { + var detail = detail || {}; + detail["sentBy"] = elt; + var event = makeEvent(eventName, detail); + var eventResult = elt.dispatchEvent(event); + return eventResult; + } + + function forEach(arr, func) { + if (arr.length) { + for (var i = 0; i < arr.length; i++) { + func(arr[i]); + } + } else { + func(arr); + } + } + + function getScript(elt) { + for (var i = 0; i < SCRIPT_ATTRIBUTES.length; i++) { + var scriptAttribute = SCRIPT_ATTRIBUTES[i]; + if (elt.hasAttribute && elt.hasAttribute(scriptAttribute)) { + return elt.getAttribute(scriptAttribute) + } + } + return null; + } + + function applyEventListeners(hypeScript, elt) { + forEach(hypeScript.eventListeners, function (eventListener) { + eventListener(elt); + }); + } + + function setScriptAttrs(values) { + SCRIPT_ATTRIBUTES = values; + } + + function getScriptSelector() { + return SCRIPT_ATTRIBUTES.map(function (attribute) { + return "[" + attribute + "]"; + }).join(", "); + } + + function isType(o, type) { + return Object.prototype.toString.call(o) === "[object " + type + "]"; + } + + function evaluate(typeOrSrc, srcOrCtx, ctxArg) { + if (isType(srcOrCtx, "Object")) { + var src = typeOrSrc; + var ctx = srcOrCtx; + var type = "expression" + } else if (isType(srcOrCtx, "String")) { + var src = srcOrCtx; + var type = typeOrSrc + var ctx = ctxArg; + } else { + var src = typeOrSrc; + var ctx = {}; + var type = "expression"; + } + ctx = ctx || {}; + var compiled = _parser.parseElement(type, _lexer.tokenize(src) ).transpile(); + var evalString = "(function(" + Object.keys(ctx).join(",") + "){return " + compiled + "})"; + // TODO parser debugging + if(false) console.log("transpile: " + compiled); + if(false) console.log("evalString: " + evalString); + var args = Object.keys(ctx).map(function (key) { + return ctx[key] + }); + if(false) console.log("args", args); + return eval(evalString).apply(null, args); + } + + function initElement(elt) { + var src = getScript(elt); + if (src) { + var tokens = _lexer.tokenize(src); + var hyperScript = _parser.parseHyperScript(tokens); + var transpiled = _parser.transpile(hyperScript); + if(true) console.log(transpiled); + var hyperscriptObj = eval(transpiled); + hyperscriptObj.applyEventListenersTo(elt); + } + } + + function ajax(method, url, callback, data) { + var xhr = new XMLHttpRequest(); + xhr.onload = function() { + callback(this.response) + }; + xhr.open(method, url); + xhr.send(JSON.stringify(data)); + } + + function typeCheck(value, typeString, nullOk) { + if (value == null && nullOk) { + return value; + } + var typeName = Object.prototype.toString.call(value).slice(8, -1); + var typeCheckValue = value && typeName === typeString; + if (typeCheckValue) { + return value; + } else { + throw new Error("Typecheck failed! Expected: " + typeString + ", Found: " + typeName); + } + } + + return { + typeCheck: typeCheck, + forEach: forEach, + triggerEvent: triggerEvent, + matchesSelector: matchesSelector, + getScript: getScript, + applyEventListeners: applyEventListeners, + setScriptAttrs: setScriptAttrs, + initElement: initElement, + evaluate: evaluate, + getScriptSelector: getScriptSelector, + ajax: ajax, + } + }(); + + //----------------------------------------------- + // Expressions + //----------------------------------------------- + + _parser.addGrammarElement("parenthesized", function (parser, tokens) { + if (tokens.matchOpToken('(')) { + var expr = parser.parseElement("expression", tokens); + tokens.requireOpToken(")"); + return { + type: "parenthesized", + expr: expr, + transpile: function () { + return "(" + parser.transpile(expr) + ")"; + } + } + } + }) + + _parser.addGrammarElement("string", function (parser, tokens) { + var stringToken = tokens.matchTokenType('STRING'); + if (stringToken) { + return { + type: "string", + token: stringToken, + transpile: function () { + if (stringToken.value.indexOf("'") === 0) { + return "'" + stringToken.value + "'"; + } else { + return '"' + stringToken.value + '"'; + } + } + } + } + }) + + _parser.addGrammarElement("number", function (parser, tokens) { + var number = tokens.matchTokenType('NUMBER'); + if (number) { + var numberToken = number; + var value = parseFloat(number.value) + return { + type: "number", + value: value, + numberToken: numberToken, + transpile: function () { + return numberToken.value; + } + } + } + }) + + _parser.addGrammarElement("idRef", function (parser, tokens) { + var elementId = tokens.matchTokenType('ID_REF'); + if (elementId) { + return { + type: "idRef", + value: elementId.value.substr(1), + transpile: function () { + return "document.getElementById('" + this.value + "')" + } + }; + } + }) + + _parser.addGrammarElement("classRef", function (parser, tokens) { + var classRef = tokens.matchTokenType('CLASS_REF'); + if (classRef) { + return { + type: "classRef", + value: classRef.value, + className: function () { + return this.value.substr(1); + }, + transpile: function () { + return "document.querySelectorAll('" + this.value + "')" + } + }; + } + }) + + _parser.addGrammarElement("attributeRef", function (parser, tokens) { + if (tokens.matchOpToken("[")) { + var name = tokens.matchTokenType("IDENTIFIER"); + var value = null; + if (tokens.matchOpToken("=")) { + value = parser.parseElement("expression", tokens); + } + tokens.requireOpToken("]"); + return { + type: "attribute_expression", + name: name.value, + value: value, + transpile: function () { + if (this.value) { + return "({name: '" + this.name + "', value: " + parser.transpile(this.value) + "})"; + } else { + return "({name: '" + this.name + "'})"; + } + } + } + } + }) + + _parser.addGrammarElement("objectLiteral", function (parser, tokens) { + if (tokens.matchOpToken("{")) { + var fields = [] + if (!tokens.matchOpToken("}")) { + do { + var name = tokens.requireTokenType("IDENTIFIER"); + tokens.requireOpToken(":"); + var value = parser.parseElement("expression", tokens); + fields.push({name: name, value: value}); + } while (tokens.matchOpToken(",")) + tokens.requireOpToken("}"); + } + return { + type: "objectLiteral", + fields: fields, + transpile: function () { + return "({" + fields.map(function (field) { + return field.name.value + ":" + parser.transpile(field.value) + }).join(", ") + "})"; + } + } + } + + + }) + + _parser.addGrammarElement("symbol", function (parser, tokens) { + var identifier = tokens.matchTokenType('IDENTIFIER'); + if (identifier) { + return { + type: "symbol", + name: identifier.value, + transpile: function () { + return identifier.value; + } + }; + } + }); + + _parser.addGrammarElement("implicitMeTarget", function (parser, tokens) { + return { + type: "implicitMeTarget", + transpile: function () { + return "[me]" + } + }; + }); + + _parser.addGrammarElement("implicitAllTarget", function (parser, tokens) { + return { + type: "implicitAllTarget", + transpile: function () { + return 'document.querySelectorAll("*")'; + } + }; + }); + + _parser.addGrammarElement("millisecondLiteral", function (parser, tokens) { + var number = tokens.requireTokenType(tokens, "NUMBER"); + var factor = 1; + if (tokens.matchToken("s")) { + factor = 1000; + } else if (tokens.matchToken("ms")) { + // do nothing + } + return { + type: "millisecondLiteral", + number: number, + factor: factor, + transpile: function () { + return factor * parseFloat(this.number.value); + } + }; + }); + + _parser.addGrammarElement("boolean", function (parser, tokens) { + var booleanLiteral = tokens.matchToken("true") || tokens.matchToken("false"); + if (booleanLiteral) { + return { + type: "boolean", + transpile: function () { + return booleanLiteral.value; + } + } + } + }); + + _parser.addGrammarElement("null", function (parser, tokens) { + if (tokens.matchToken('null')) { + return { + type: "null", + transpile: function () { + return "null"; + } + } + } + }); + + _parser.addGrammarElement("arrayLiteral", function (parser, tokens) { + if (tokens.matchOpToken('[')) { + var values = []; + if (!tokens.matchOpToken(']')) { + do { + var expr = parser.parseElement("expression", tokens); + if (expr == null) { + parser.raiseParseError(tokens, "Expected an expression"); + } + values.push(expr); + } while(tokens.matchOpToken(",")) + tokens.requireOpToken("]"); + } + return { + type: "arrayLiteral", + values:values, + transpile: function () { + return "[" + values.map(function(v){ return parser.transpile(v) }).join(", ") + "]"; + } + } + } + }); + + _parser.addGrammarElement("blockLiteral", function (parser, tokens) { + if (tokens.matchOpToken('\\')) { + var args = [] + var arg1 = tokens.matchTokenType("IDENTIFIER"); + if (arg1) { + args.push(arg1); + while (tokens.matchOpToken(",")) { + args.push(tokens.requireTokenType("IDENTIFIER")); + } + } + // TODO compound op token + tokens.requireOpToken("-"); + tokens.requireOpToken(">"); + var expr = parser.parseElement("expression", tokens); + if (expr == null) { + parser.raiseParseError(tokens, "Expected an expression"); + } + return { + type: "blockLiteral", + args: args, + expr: expr, + transpile: function () { + return "function(" + args.map(function (arg) { + return arg.value + }).join(", ") + "){ return " + + parser.transpile(expr) + " }"; + } + } + } + }); + + _parser.addGrammarElement("leaf", function (parser, tokens) { + return parser.parseAnyOf(["parenthesized", "boolean", "null", "string", "number", "idRef", "classRef", "symbol", "propertyRef", "objectLiteral", "arrayLiteral", "blockLiteral"], tokens) + }); + + _parser.addGrammarElement("propertyAccess", function (parser, tokens, root) { + if (tokens.matchOpToken(".")) { + var prop = tokens.requireTokenType("IDENTIFIER"); + var propertyAccess = { + type: "propertyAccess", + root: root, + prop: prop, + transpile: function () { + return parser.transpile(root) + "." + prop.value; + } + }; + return _parser.parseElement("indirectExpression", tokens, propertyAccess); + } + }); + + _parser.addGrammarElement("functionCall", function (parser, tokens, root) { + if (tokens.matchOpToken("(")) { + var args = []; + do { + args.push(parser.parseElement("expression", tokens)); + } while (tokens.matchOpToken(",")) + tokens.requireOpToken(")"); + var functionCall = { + type: "functionCall", + root: root, + args: args, + transpile: function () { + return parser.transpile(root) + "(" + args.map(function (arg) { + return parser.transpile(arg) + }).join(",") + ")" + } + }; + return _parser.parseElement("indirectExpression", tokens, functionCall); + } + }); + + _parser.addGrammarElement("indirectExpression", function (parser, tokens, root) { + var propAccess = parser.parseElement("propertyAccess", tokens, root); + if (propAccess) { + return propAccess; + } + + var functionCall = parser.parseElement("functionCall", tokens, root); + if (functionCall) { + return functionCall; + } + + return root; + }); + + _parser.addGrammarElement("primaryExpression", function (parser, tokens) { + var leaf = parser.parseElement("leaf", tokens); + if (leaf) { + return parser.parseElement("indirectExpression", tokens, leaf); + } + parser.raiseParseError(tokens, "Unexpected value: " + tokens.currentToken().value); + }); + + _parser.addGrammarElement("postfixExpression", function (parser, tokens) { + var root = parser.parseElement("primaryExpression", tokens); + if (tokens.matchOpToken(":")) { + var typeName = tokens.requireTokenType("IDENTIFIER"); + var nullOk = !tokens.matchOpToken("!"); + return { + type: "typeCheck", + typeName: typeName, + root: root, + nullOk: nullOk, + transpile: function () { + return "_hyperscript.runtime.typeCheck(" + parser.transpile(root) + ", '" + typeName.value + "', " + nullOk + ")"; + } + } + } else { + return root; + } + }); + + _parser.addGrammarElement("logicalNot", function (parser, tokens) { + if (tokens.matchToken("not")) { + var root = parser.parseElement("unaryExpression", tokens); + return { + type: "logicalNot", + root: root, + transpile: function () { + return "!" + parser.transpile(root); + } + }; + } + }); + + _parser.addGrammarElement("negativeNumber", function (parser, tokens) { + if (tokens.matchOpToken("-")) { + var root = parser.parseElement("unaryExpression", tokens); + return { + type: "negativeNumber", + root: root, + transpile: function () { + return "-" + parser.transpile(root); + } + }; + } + }); + + _parser.addGrammarElement("unaryExpression", function (parser, tokens) { + return parser.parseAnyOf(["logicalNot", "negativeNumber", "postfixExpression"], tokens); + }); + + _parser.addGrammarElement("mathOperator", function (parser, tokens) { + var expr = parser.parseElement("unaryExpression", tokens); + var mathOp, initialMathOp = null; + mathOp = tokens.matchAnyOpToken("+", "-", "*", "/", "%") + while (mathOp) { + initialMathOp = initialMathOp || mathOp; + if(initialMathOp.value !== mathOp.value) { + parser.raiseParseError(tokens, "You must parenthesize math operations with different operators") + } + var rhs = parser.parseElement("unaryExpression", tokens); + expr = { + type: "mathOperator", + operator: mathOp.value, + lhs: expr, + rhs: rhs, + transpile: function () { + return parser.transpile(this.lhs) + " " + this.operator + " " + parser.transpile(this.rhs); + } + } + mathOp = tokens.matchAnyOpToken("+", "-", "*", "/", "%") + } + return expr; + }); + + _parser.addGrammarElement("mathExpression", function (parser, tokens) { + return parser.parseAnyOf(["mathOperator", "unaryExpression"], tokens); + }); + + _parser.addGrammarElement("comparisonOperator", function (parser, tokens) { + var expr = parser.parseElement("mathExpression", tokens); + var comparisonOp, initialComparisonOp = null; + comparisonOp = tokens.matchAnyOpToken("<", ">", "<=", ">=", "==", "===") + while (comparisonOp) { + initialComparisonOp = initialComparisonOp || comparisonOp; + if(initialComparisonOp.value !== comparisonOp.value) { + parser.raiseParseError(tokens, "You must parenthesize comparison operations with different operators") + } + var rhs = parser.parseElement("mathExpression", tokens); + expr = { + type: "comparisonOperator", + operator: comparisonOp.value, + lhs: expr, + rhs: rhs, + transpile: function () { + return parser.transpile(this.lhs) + " " + this.operator + " " + parser.transpile(this.rhs); + } + } + comparisonOp = tokens.matchAnyOpToken("<", ">", "<=", ">=", "==", "===") + } + return expr; + }); + + _parser.addGrammarElement("comparisonExpression", function (parser, tokens) { + return parser.parseAnyOf(["comparisonOperator", "mathExpression"], tokens); + }); + + _parser.addGrammarElement("logicalOperator", function (parser, tokens) { + var expr = parser.parseElement("comparisonExpression", tokens); + var logicalOp, initialLogicalOp = null; + logicalOp = tokens.matchToken("and") || tokens.matchToken("or"); + while (logicalOp) { + initialLogicalOp = initialLogicalOp || logicalOp; + if(initialLogicalOp.value !== logicalOp.value) { + parser.raiseParseError(tokens, "You must parenthesize logical operations with different operators") + } + var rhs = parser.parseElement("comparisonExpression", tokens); + expr = { + type: "logicalOperator", + operator: logicalOp.value, + lhs: expr, + rhs: rhs, + transpile: function () { + return parser.transpile(this.lhs) + " " + (this.operator === "and" ? " && " : " || ") + " " + parser.transpile(this.rhs); + } + } + logicalOp = tokens.matchToken("and") || tokens.matchToken("or"); + } + return expr; + }); + + _parser.addGrammarElement("logicalExpression", function (parser, tokens) { + return parser.parseAnyOf(["logicalOperator", "mathExpression"], tokens); + }); + + _parser.addGrammarElement("expression", function (parser, tokens) { + return parser.parseElement("logicalExpression", tokens); + }); + + _parser.addGrammarElement("target", function (parser, tokens) { + var value = parser.parseAnyOf(["symbol", "classRef", "idRef"], tokens); + if (value == null) { + parser.raiseParseError(tokens, "Expected a valid target expression"); + } + return { + type: "target", + value: value, + transpile: function (context) { + if (value.type === "classRef") { + return parser.transpile(value); + } else if (value.type === "idRef") { + return "[" + parser.transpile(value) + "]"; + } else { + return "[" + parser.transpile(value) + "]"; //TODO, check if array? + } + } + }; + }); + + _parser.addGrammarElement("command", function (parser, tokens) { + return parser.parseAnyOf(["onCmd", "addCmd", "removeCmd", "toggleCmd", "waitCmd", "sendCmd", + "takeCmd", "logCmd", "callCmd", "putCmd", "ifCmd", "ajaxCmd"], tokens); + }) + + _parser.addGrammarElement("commandList", function (parser, tokens) { + var cmd = parser.parseElement("command", tokens); + if (cmd) { + tokens.matchToken("then"); + cmd.next = parser.parseElement("commandList", tokens); + return cmd; + } + }) + + _parser.addGrammarElement("hyperscript", function (parser, tokens) { + var eventListeners = [] + do { + eventListeners.push(parser.parseElement("eventListener", tokens)); + } while (tokens.matchToken("end") && tokens.hasMore()) + if (tokens.hasMore()) { + parser.raiseParseError(tokens); + } + return { + type: "hyperscript", + eventListeners: eventListeners, + transpile: function () { + return "(function(){\n" + + "var eventListeners = []\n" + + eventListeners.map(function (el) { + return "eventListeners.push(" + parser.transpile(el) + ");\n" + }) + + " function applyEventListenersTo(elt) { _hyperscript.runtime.applyEventListeners(this, elt) }" + + " return {eventListeners:eventListeners, applyEventListenersTo:applyEventListenersTo}\n" + + "})()" + } + }; + }) + + + _parser.addGrammarElement("eventListener", function (parser, tokens) { + tokens.requireToken("on"); + var on = parser.parseElement("dotPath", tokens); + if (on == null) { + parser.raiseParseError(tokens, "Expected event name") + } + if (tokens.matchToken("from")) { + var from = parser.parseElement("target", tokens); + if (from == null) { + parser.raiseParseError(tokens, "Expected target value") + } + } else { + var from = parser.parseElement("implicitMeTarget", tokens); + } + var start = parser.parseElement("commandList", tokens); + var eventListener = { + type: "eventListener", + on: on, + from: from, + start: start, + transpile: function () { + return "(function(me){" + + "var my = me;\n" + + "_hyperscript.runtime.forEach( " + parser.transpile(from) + ", function(target){\n" + + " target.addEventListener('" + parser.transpile(on) + "', function(event){\n" + + parser.transpile(start) + + " })\n" + + "})\n" + + "})" + } + }; + return eventListener; + }); + + _parser.addGrammarElement("addCmd", function (parser, tokens) { + if (tokens.matchToken("add")) { + var classRef = parser.parseElement("classRef", tokens); + var attributeRef = null; + if (classRef == null) { + attributeRef = parser.parseElement("attributeRef", tokens); + if (attributeRef == null) { + parser.raiseParseError(tokens, "Expected either a class reference or attribute expression") + } + } + + if (tokens.matchToken("to")) { + var to = parser.parseElement("target", tokens); + } else { + var to = parser.parseElement("implicitMeTarget"); + } + + return { + type: "addCmd", + classRef: classRef, + attributeRef: attributeRef, + to: to, + transpile: function () { + if (this.classRef) { + return "_hyperscript.runtime.forEach( " + parser.transpile(to) + ", function (target) {" + + " target.classList.add('" + classRef.className() + "')" + + "})"; + } else { + return "_hyperscript.runtime.forEach( " + parser.transpile(to) + ", function (target) {" + + " target.setAttribute('" + attributeRef.name + "', " + parser.transpile(attributeRef) + ".value)" + + "})"; + } + } + } + } + }); + + _parser.addGrammarElement("removeCmd", function (parser, tokens) { + if (tokens.matchToken("remove")) { + var classRef = parser.parseElement("classRef", tokens); + var attributeRef = null; + var elementExpr = null; + if (classRef == null) { + attributeRef = parser.parseElement("attributeRef", tokens); + if (attributeRef == null) { + elementExpr = parser.parseElement("expression", tokens) + if (elementExpr == null) { + parser.raiseParseError(tokens, "Expected either a class reference, attribute expression or value expression"); + } + } + } + if (tokens.matchToken("from")) { + var from = parser.parseElement("target", tokens); + } else { + var from = parser.parseElement("implicitMeTarget"); + } + + return { + type: "removeCmd", + classRef: classRef, + attributeRef: attributeRef, + elementExpr: elementExpr, + from: from, + transpile: function () { + if (this.elementExpr) { + return "_hyperscript.runtime.forEach( " + parser.transpile(elementExpr) + ", function (target) {" + + " target.parentElement.removeChild(target)" + + "})"; + } else { + if (this.classRef) { + return "_hyperscript.runtime.forEach( " + parser.transpile(from) + ", function (target) {" + + " target.classList.remove('" + classRef.className() + "')" + + "})"; + } else { + return "_hyperscript.runtime.forEach( " + parser.transpile(from) + ", function (target) {" + + " target.removeAttribute('" + attributeRef.name + "')" + + "})"; + } + } + } + } + } + }); + + _parser.addGrammarElement("toggleCmd", function (parser, tokens) { + if (tokens.matchToken("toggle")) { + var classRef = parser.parseElement("classRef", tokens); + var attributeRef = null; + if (classRef == null) { + attributeRef = parser.parseElement("attributeRef", tokens); + if (attributeRef == null) { + parser.raiseParseError(tokens, "Expected either a class reference or attribute expression") + } + } + if (tokens.matchToken("on")) { + var on = parser.parseElement("target", tokens); + } else { + var on = parser.parseElement("implicitMeTarget"); + } + return { + type: "toggleCmd", + classRef: classRef, + attributeRef: attributeRef, + on: on, + transpile: function () { + if (this.classRef) { + return "_hyperscript.runtime.forEach( " + parser.transpile(on) + ", function (target) {" + + " target.classList.toggle('" + classRef.className() + "')" + + "})"; + } else { + return "_hyperscript.runtime.forEach( " + parser.transpile(on) + ", function (target) {" + + " if(target.hasAttribute('" + attributeRef.name + "')) {\n" + + " target.removeAttribute('" + attributeRef.name + "');\n" + + " } else { \n" + + " target.setAttribute('" + attributeRef.name + "', " + parser.transpile(attributeRef) + ".value)" + + " }" + + "})"; + } + } + } + } + }) + + _parser.addGrammarElement("waitCmd", function (parser, tokens) { + if (tokens.matchToken("wait")) { + var time = parser.parseElement('millisecondLiteral', tokens); + return { + type: "waitCmd", + time: time, + transpile: function () { + var capturedNext = this.next; + delete this.next; + return "setTimeout(function () { " + parser.transpile(capturedNext) + " }, " + parser.transpile(this.time) + ")"; + } + } + } + }) + + _parser.addGrammarElement("dotPath", function (parser, tokens) { + var root = tokens.matchTokenType("IDENTIFIER"); + if (root) { + var path = [root.value]; + while (tokens.matchOpToken(".")) { + path.push(tokens.requireTokenType("IDENTIFIER").value); + } + return { + type: "dotPath", + path: path, + transpile: function () { + return path.join("."); + } + } + } + }); + + _parser.addGrammarElement("sendCmd", function (parser, tokens) { + if (tokens.matchToken("send")) { + + var eventName = parser.parseElement("dotPath", tokens); + + var details = parser.parseElement("objectLiteral", tokens); + if (tokens.matchToken("to")) { + var to = parser.parseElement("target", tokens); + } else { + var to = parser.parseElement("implicitMeTarget"); + } + + return { + type: "sendCmd", + eventName: eventName, + details: details, + to: to, + transpile: function () { + return "_hyperscript.runtime.forEach( " + parser.transpile(to) + ", function (target) {" + + " _hyperscript.runtime.triggerEvent(target, '" + parser.transpile(eventName) + "'," + parser.transpile(details, "{}") + ")" + + "})"; + } + } + } + }) + + _parser.addGrammarElement("takeCmd", function (parser, tokens) { + if (tokens.matchToken("take")) { + var classRef = tokens.requireTokenType(tokens, "CLASS_REF"); + + if (tokens.matchToken("from")) { + var from = parser.parseElement("target", tokens); + } else { + var from = parser.parseElement("implicitAllTarget") + } + return { + type: "takeCmd", + classRef: classRef, + from: from, + transpile: function () { + var clazz = this.classRef.value.substr(1); + return " _hyperscript.runtime.forEach(" + parser.transpile(from) + ", function (target) { target.classList.remove('" + clazz + "') }); " + + "me.classList.add('" + clazz + "');"; + } + } + } + }) + + _parser.addGrammarElement("logCmd", function (parser, tokens) { + if (tokens.matchToken("log")) { + var exprs = [parser.parseElement("expression", tokens)]; + while (tokens.matchOpToken(",")) { + exprs.push(parser.parseElement("expression", tokens)); + } + if (tokens.matchToken("with")) { + var withExpr = parser.parseElement("expression", tokens); + } + return { + type: "logCmd", + exprs: exprs, + withExpr: withExpr, + transpile: function () { + if (withExpr) { + return parser.transpile(withExpr) + "(" + exprs.map(function (expr) { + return parser.transpile(expr) + }).join(", ") + ")"; + } else { + return "console.log(" + exprs.map(function (expr) { + return parser.transpile(expr) + }).join(", ") + ")"; + } + } + }; + } + }) + + _parser.addGrammarElement("callCmd", function (parser, tokens) { + if (tokens.matchToken("call")) { + return { + type: "callCmd", + expr: parser.parseElement("expression", tokens), + transpile: function () { + return "var it = " + parser.transpile(this.expr); + } + } + } + }) + + _parser.addGrammarElement("putCmd", function (parser, tokens) { + if (tokens.matchToken("put")) { + + var value = parser.parseElement("expression", tokens); + + var operation = tokens.matchToken("into") || + tokens.matchToken("before") || + tokens.matchToken("afterbegin") || + tokens.matchToken("beforeend") || + tokens.matchToken("after"); + + if (operation == null) { + parser.raiseParseError(tokens, "Expected one of 'into', 'before', 'afterbegin', 'beforeend', 'after'") + } + var target = parser.parseElement("target", tokens); + var propPath = [] + while (tokens.matchOpToken(".")) { + propPath.push(tokens.requireTokenType("IDENTIFIER").value) + } + + var directWrite = propPath.length === 0 && operation.value === "into"; + var symbolWrite = directWrite && target.value.type === "symbol"; + if (directWrite && !symbolWrite) { + parser.raiseParseError(tokens, "Can only put directly into symbols, not references") + } + + return { + type: "putCmd", + target: target, + propPath: propPath, + op: operation.value, + symbolWrite: symbolWrite, + value: value, + transpile: function () { + if (this.symbolWrite) { + return "var " + target.value.name + " = " + parser.transpile(value); + } else { + var dotPath = propPath.length === 0 ? "" : "." + propPath.join("."); + if (this.op === "into") { + return "_hyperscript.runtime.forEach( " + parser.transpile(target) + ", function (target) {" + + " target" + dotPath + "=" + parser.transpile(value) + + "})"; + } else if (this.op === "before") { + return "_hyperscript.runtime.forEach( " + parser.transpile(target) + ", function (target) {" + + " target" + dotPath + ".insertAdjacentHTML('beforebegin', " + parser.transpile(value) + ")" + + "})"; + } else if (this.op === "afterbegin") { + return "_hyperscript.runtime.forEach( " + parser.transpile(target) + ", function (target) {" + + " target" + dotPath + ".insertAdjacentHTML('afterbegin', " + parser.transpile(value) + ")" + + "})"; + } else if (this.op === "beforeend") { + return "_hyperscript.runtime.forEach( " + parser.transpile(target) + ", function (target) {" + + " target" + dotPath + ".insertAdjacentHTML('beforeend', " + parser.transpile(value) + ")" + + "})"; + } else if (this.op === "after") { + return "_hyperscript.runtime.forEach( " + parser.transpile(target) + ", function (target) {" + + " target" + dotPath + ".insertAdjacentHTML('afterend', " + parser.transpile(value) + ")" + + "})"; + } + } + } + } + } + }) + + _parser.addGrammarElement("ifCmd", function (parser, tokens) { + if (tokens.matchToken("if")) { + var expr = parser.parseElement("expression", tokens); + var trueBranch = parser.parseElement("commandList", tokens); + if (tokens.matchToken("else")) { + var falseBranch = parser.parseElement("commandList", tokens); + } + if (tokens.hasMore()) { + tokens.requireToken("end"); + } + return { + type: "ifCmd", + expr: expr, + trueBranch: trueBranch, + falseBranch: falseBranch, + transpile: function () { + return "if(" + parser.transpile(expr) + "){" + "" + parser.transpile(trueBranch) + "}" + + " else {" + parser.transpile(falseBranch, "") + "}" + + } + } + } + }) + + _parser.addGrammarElement("ajaxCmd", function (parser, tokens) { + if (tokens.matchToken("ajax")) { + var method = tokens.matchToken("GET") || tokens.matchToken("POST"); + if (method == null) { + parser.raiseParseError(tokens, "Requires either GET or POST"); + } + if (method.value === "GET") { + tokens.requireToken("from"); + } else { + tokens.requireToken("to"); + } + + var url = parser.parseElement("string", tokens); + if (url == null) { + parser.raiseParseError(tokens, "Requires a URL"); + } + if (tokens.matchToken("with")) { + tokens.requireToken("body"); + var data = parser.parseElement("expression", tokens); + if (data == null) { + parser.raiseParseError(tokens, "Requires a URL"); + } + } + return { + type: "requestCommand", + method: method, + transpile: function () { + var capturedNext = this.next; + delete this.next; + return "_hyperscript.runtime.ajax('" + method.value + "', " + + parser.transpile(url) + ", " + + "function(it){ " + parser.transpile(capturedNext) + " }," + + parser.transpile(data, "null") + ")"; + } + }; + } + }) + + //----------------------------------------------- + // API + //----------------------------------------------- + + function start(scriptAttrs) { + if (scriptAttrs) { + _runtime.setScriptAttrs(scriptAttrs); + } + var fn = function () { + var elements = document.querySelectorAll(_runtime.getScriptSelector()); + _runtime.forEach(elements, function (elt) { + init(elt); + }) + }; + if (document.readyState !== 'loading') { + fn(); + } else { + document.addEventListener('DOMContentLoaded', fn); + } + return true; + } + + function init(elt) { + _runtime.initElement(elt); + } + + function evaluate(str) { + return _runtime.evaluate(str); + } + + return { + lexer: _lexer, + parser: _parser, + runtime: _runtime, + evaluate: evaluate, + init: init, + start: start + } + } + )() +}));
\ No newline at end of file diff --git a/www/attributes/hx-error-url.md b/www/attributes/hx-error-url.md deleted file mode 100644 index b6d2715b..00000000 --- a/www/attributes/hx-error-url.md +++ /dev/null @@ -1,24 +0,0 @@ ---- -layout: layout.njk -title: </> htmx - hx-error-url ---- - -## `hx-error-url` - -The `hx-error-url` attribute allows you to send client-side errors to a specified URL. It is typically put on the -body tag, so all errors are caught and send to the server. - -```html -<body hx-error-url="/errors">\ - -</body> -``` -When a client side error is caught by htmx it will be `POST`-ed to the given URL, with the following JSON format: - -```json - { "elt": elt.id, "event": eventName, "detail" : detail } -``` - -### Notes - -* `hx-error-url` is inherited and can be placed on a parent element diff --git a/www/docs.md b/www/docs.md index c1007ae3..8b3818b6 100644 --- a/www/docs.md +++ b/www/docs.md @@ -568,8 +568,6 @@ If you set a logger at `htmx.logger`, every event will be logged. This can be v } ``` -Htmx can also send errors to a URL that is specified with the [hx-error-url](/attributes/hx-error-url) attributes. This can be useful for debugging client-side issues. - Htmx includes a helper method: ```javascript diff --git a/www/reference.md b/www/reference.md index 873715f6..c9747666 100644 --- a/www/reference.md +++ b/www/reference.md @@ -13,7 +13,6 @@ title: </> htmx - Attributes | [`hx-boost`](/attributes/hx-boost) | progressively enhances anchors and forms to use AJAX requests | [`hx-confirm`](/attributes/hx-confirm) | shows a confim() dialog before issuing a request | [`hx-delete`](/attributes/hx-delete) | issues a `DELETE` to the specified URL -| [`hx-error-url`](/attributes/hx-error-url) | a URL to send client-side errors to | [`hx-ext`](/attributes/hx-ext) | extensions to use for this element | [`hx-get`](/attributes/hx-get) | issues a `GET` to the specified URL | [`hx-history-elt`](/attributes/hx-history-elt) | the element to snapshot and restore during history navigation |