diff --git a/src/htmx.js b/src/htmx.js index 54761675..759c9edc 100644 --- a/src/htmx.js +++ b/src/htmx.js @@ -980,7 +980,7 @@ return (function () { } if (detail.error) { logError(detail.error); - triggerEvent(elt, "error.htmx", {errorDetail:detail}) + triggerEvent(elt, "error.htmx", {errorInfo:detail}) } var eventResult = elt.dispatchEvent(event); withExtensions(elt, function (extension) { diff --git a/test/lib/_hyperscript.js b/test/lib/_hyperscript.js index 97697430..bf6a5495 100644 --- a/test/lib/_hyperscript.js +++ b/test/lib/_hyperscript.js @@ -78,6 +78,9 @@ function makeTokensObject(tokens, consumed, source) { + var ignoreWhiteSpace = true; + matchTokenType("WHITESPACE"); // consume any initial whitespace + function raiseError(tokens, error) { _parser.raiseParseError(tokens, error); } @@ -141,9 +144,22 @@ function consumeToken() { var match = tokens.shift(); consumed.push(match); + if(ignoreWhiteSpace) { + matchTokenType("WHITESPACE"); // consume any whitespace until the next token + } return match; } + function consumeUntilWhitespace() { + var tokenList = []; + ignoreWhiteSpace = false; + while (currentToken() && currentToken().type !== "WHITESPACE") { + tokenList.push(consumeToken()); + } + ignoreWhiteSpace = true; + return tokenList; + } + function hasMore() { return tokens.length > 0; } @@ -164,7 +180,8 @@ list: tokens, source: source, hasMore: hasMore, - currentToken: currentToken + currentToken: currentToken, + consumeUntilWhitespace: consumeUntilWhitespace } } @@ -177,11 +194,12 @@ var lastToken = ""; while (position < source.length) { - consumeWhitespace(); if (currentChar() === "-" && nextChar() === "-") { consumeComment(); } else { - if (!possiblePrecedingSymbol() && currentChar() === "." && isAlpha(nextChar())) { + if (isWhitespace(currentChar())) { + tokens.push(consumeWhitespace()); + } else if (!possiblePrecedingSymbol() && currentChar() === "." && isAlpha(nextChar())) { tokens.push(consumeClassReference()); } else if (!possiblePrecedingSymbol() && currentChar() === "#" && isAlpha(nextChar())) { tokens.push(consumeIdReference()); @@ -281,7 +299,7 @@ function consumeOp() { var value = consumeChar(); // consume leading char while (currentChar() && OP_TABLE[value + currentChar()]) { - value += consumeChar(); + value += consumeChar(); } var op = makeOpToken(OP_TABLE[value], value); op.value = value; @@ -329,13 +347,18 @@ } function consumeWhitespace() { + var whitespace = makeToken("WHITESPACE"); + var value = ""; while (currentChar() && isWhitespace(currentChar())) { if (isNewline(currentChar())) { column = 0; line++; } - consumeChar(); + value += consumeChar(); } + whitespace.value = value; + whitespace.end = position; + return whitespace; } } @@ -458,6 +481,32 @@ } } + function evalTarget(root, path) { + if (root.length) { + var last = root; + } else { + var last = [root]; + } + + while (path.length > 0) { + var prop = path.shift(); + var next = [] + // flat map + for (var i = 0; i < last.length; i++) { + var element = last[i]; + var nextVal = element[prop]; + if (nextVal && nextVal.length) { + next = next.concat(nextVal); + } else { + next.push(nextVal); + } + } + last = next; + } + + return last; + } + function getScript(elt) { for (var i = 0; i < SCRIPT_ATTRIBUTES.length; i++) { var scriptAttribute = SCRIPT_ATTRIBUTES[i]; @@ -499,7 +548,7 @@ var ctx = ctxArg; } else { var src = typeOrSrc; - var ctx = {}; + var ctx = {}; var type = "expression"; } ctx = ctx || {}; @@ -552,6 +601,7 @@ return { typeCheck: typeCheck, forEach: forEach, + evalTarget: evalTarget, triggerEvent: triggerEvent, matchesSelector: matchesSelector, getScript: getScript, @@ -599,6 +649,20 @@ } }) + _parser.addGrammarElement("nakedString", function (parser, tokens) { + if (tokens.hasMore()) { + var tokenArr = tokens.consumeUntilWhitespace(); + tokens.matchTokenType("WHITESPACE"); + return { + type: "nakedString", + tokens: tokenArr, + transpile: function () { + return "'" + tokenArr.map(function(t){return t.value}).join("") + "'"; + } + } + } + }) + _parser.addGrammarElement("number", function (parser, tokens) { var number = tokens.matchTokenType('NUMBER'); if (number) { @@ -1019,21 +1083,24 @@ }); _parser.addGrammarElement("target", function (parser, tokens) { - var value = parser.parseAnyOf(["symbol", "classRef", "idRef"], tokens); - if (value == null) { + var root = parser.parseAnyOf(["symbol", "classRef", "idRef"], tokens); + if (root == null) { parser.raiseParseError(tokens, "Expected a valid target expression"); } + + var propPath = [] + while (tokens.matchOpToken(".")) { + propPath.push(tokens.requireTokenType("IDENTIFIER").value) + } + 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? - } + propPath: propPath, + root: root, + transpile: function () { + return "_hyperscript.runtime.evalTarget(" + parser.transpile(root) + ", [" + propPath.map(function (prop) { + return "\"" + prop + "\"" + }).join(", ") + "])"; } }; }); @@ -1091,6 +1158,15 @@ } else { var from = parser.parseElement("implicitMeTarget", tokens); } + + var args = []; + if (tokens.matchOpToken("(")) { + do { + args.push(tokens.requireTokenType('IDENTIFIER')); + } while (tokens.matchOpToken(",")) + tokens.requireOpToken(')') + } + var start = parser.parseElement("commandList", tokens); var eventListener = { type: "eventListener", @@ -1102,6 +1178,7 @@ "var my = me;\n" + "_hyperscript.runtime.forEach( " + parser.transpile(from) + ", function(target){\n" + " target.addEventListener('" + parser.transpile(on) + "', function(event){\n" + + args.map(function(arg){return "var " + arg.value + " = event.detail." + arg.value + ";"}).join("\n") + "\n" + parser.transpile(start) + " })\n" + "})\n" + @@ -1301,14 +1378,24 @@ } else { var from = parser.parseElement("implicitAllTarget") } + + if (tokens.matchToken("for")) { + var forElt = parser.parseElement("target", tokens); + } else { + var forElt = parser.parseElement("implicitMeTarget") + } + return { type: "takeCmd", classRef: classRef, from: from, + forElt: forElt, 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 + "');"; + "_hyperscript.runtime.forEach( " + parser.transpile(forElt) + ", function (target) {" + + " target.classList.add('" + clazz + "')" + + "})"; } } } @@ -1369,13 +1456,9 @@ 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"; + var directWrite = target.propPath.length === 0 && operation.value === "into"; + var symbolWrite = directWrite && target.root.type === "symbol"; if (directWrite && !symbolWrite) { parser.raiseParseError(tokens, "Can only put directly into symbols, not references") } @@ -1383,34 +1466,33 @@ 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); + return "var " + target.root.name + " = " + parser.transpile(value); } else { - var dotPath = propPath.length === 0 ? "" : "." + propPath.join("."); if (this.op === "into") { + var lastProperty = target.propPath.pop(); // steal last property for assignment return "_hyperscript.runtime.forEach( " + parser.transpile(target) + ", function (target) {" + - " target" + dotPath + "=" + parser.transpile(value) + + " target." + lastProperty + "=" + parser.transpile(value) + "})"; } else if (this.op === "before") { return "_hyperscript.runtime.forEach( " + parser.transpile(target) + ", function (target) {" + - " target" + dotPath + ".insertAdjacentHTML('beforebegin', " + parser.transpile(value) + ")" + + " target.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) + ")" + + " target.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) + ")" + + " target.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) + ")" + + " target.insertAdjacentHTML('afterend', " + parser.transpile(value) + ")" + "})"; } } @@ -1452,20 +1534,17 @@ if (method.value === "GET") { tokens.requireToken("from"); } else { - tokens.requireToken("to"); + if (!tokens.matchToken("to")) { + var data = parser.parseElement("expression", tokens); + 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"); - } + var url = parser.parseElement("nakedString", tokens); } + return { type: "requestCommand", method: method, @@ -1473,8 +1552,8 @@ var capturedNext = this.next; delete this.next; return "_hyperscript.runtime.ajax('" + method.value + "', " + - parser.transpile(url) + ", " + - "function(it){ " + parser.transpile(capturedNext) + " }," + + parser.transpile(url) + ", " + + "function(response){ " + parser.transpile(capturedNext) + " }," + parser.transpile(data, "null") + ")"; } }; diff --git a/www/docs.md b/www/docs.md index 29e803c1..659e80dc 100644 --- a/www/docs.md +++ b/www/docs.md @@ -581,42 +581,70 @@ if you want to log everything while developing. **NOTE: hyperscript is in very early alpha** -Htmx has a sister project named [hyperscript](https://hyperscript.org). Hyperscript is a small scripting language -designed to be expressive, making it ideal for embedding directly in HTML, handling custom events, etc. The language -is inspired by [HyperTalk](http://hypercard.org/HyperTalk%20Reference%202.4.pdf), javascript, [gosu](https://gosu-lang.github.io/) -and others. +Hyperscript is a small scripting language designed to be expressive, making it ideal for embedding directly in HTML, +handling custom events, etc. The language is inspired by [HyperTalk](http://hypercard.org/HyperTalk%20Reference%202.4.pdf), +javascript, [gosu](https://gosu-lang.github.io/) and others. -The language can be explored more fully on its website: +You can explore the language more fully on its main website: -### Catching Events with Hyperscript +### Events & Hyperscript -A primary use case of hyperscript is catching and responding to events triggered by htmx. +Hyperscript was designed to help address features and functionality from intercooler.js that are not implemented in htmx +directly, in a more flexible and open manner. One of its prime features is the ability to respond to arbitrary events +on a DOM element, using the `on` syntax: -Here is an example with an element that is shown for 5 seconds, then removes the element: +```html +
+ ... +
+``` + +This will log `Settled!` to the console when the `afterSettle.htmx` event is triggered. + +#### intercooler.js features & hyperscript implementations + +Below are some examples of intercooler features and the hyperscript equivalent. + +##### `ic-remove-after` + +Intercooler provided the [`ic-remove-after`](http://intercoolerjs.org/attributes/ic-remove-after.html) attribute +for removing an element after a given amount of time. + +In hyperscript this is implemented like so: ```html
Here is a temporary message!
``` -Here is an example that posts all htmx errors to a server URL: +##### `ic-post-errors-to` + +Intercooler provided the [`ic-post-errors-to`](http://intercoolerjs.org/attributes/ic-post-errors-to.html) attribute +for posting errors that occured during requests and responses. + +In hyperscript similar functionality is implemented like so: ```html - + ... ``` -Here is an example that takes an `active` class from other tabs: +##### `ic-switch-class` + +Intercooler provided the [`ic-switch-class`](http://intercoolerjs.org/attributes/ic-switch-class.html) attribute, which +let you switch a class between siblings. + +In hyperscript you can implement similar functionality like so: ```html -
- Tab 1 +
+ Tab 1 Tab 2 Tab 3
-
...
+
Tab 1 Content
``` ## [Configuring htmx](#config)