hx-on attribute

This commit is contained in:
Carson Gross
2023-03-31 06:44:01 -06:00
parent 2713a3ada0
commit b4e6c5cbd5
8 changed files with 232 additions and 5 deletions

View File

@@ -1066,7 +1066,7 @@ return (function () {
var WHITESPACE = /\s/;
var WHITESPACE_OR_COMMA = /[\s,]/;
var SYMBOL_START = /[_$a-zA-Z]/;
var SYMBOL_CONT = /[_$a-zA-Z0-9:-]/;
var SYMBOL_CONT = /[_$a-zA-Z0-9]/;
var STRINGISH_START = ['"', "'", "/"];
var NOT_WHITESPACE = /[^\s]/;
function tokenizeString(str) {
@@ -1842,7 +1842,7 @@ return (function () {
nodeData.onHandlers ||= {};
var func = new Function("event", code + "; return;");
var listener = elt.addEventListener(eventName, function (e) {
return func(e);
return func.call(elt, e);
});
nodeData.onHandlers[eventName] = listener;
return {nodeData, code, func, listener};

96
test/attributes/hx-on.js Normal file
View File

@@ -0,0 +1,96 @@
describe("hx-on attribute", function() {
beforeEach(function () {
this.server = makeServer();
clearWorkArea();
});
afterEach(function () {
this.server.restore();
clearWorkArea();
});
it("can handle basic events w/ no other attributes", function () {
var btn = make("<button hx-on='click: window.foo = true'>Foo</button>");
btn.click();
window.foo.should.equal(true);
delete window.foo;
});
it("can modify a parameter via htmx:configRequest", function () {
this.server.respondWith("POST", "/test", function (xhr) {
var params = parseParams(xhr.requestBody);
xhr.respond(200, {}, params.foo);
});
var btn = make("<button hx-on='htmx:configRequest: event.detail.parameters.foo = \"bar\"' hx-post='/test'>Foo</button>");
btn.click();
this.server.respond();
btn.innerText.should.equal("bar");
});
it("can cancel an event via preventDefault for htmx:configRequest", function () {
this.server.respondWith("POST", "/test", function (xhr) {
xhr.respond(200, {}, "<button>Bar</button>");
});
var btn = make("<button hx-on='htmx:configRequest: event.preventDefault()' hx-post='/test' hx-swap='outerHTML'>Foo</button>");
btn.click();
this.server.respond();
btn.innerText.should.equal("Foo");
});
it("can respond to kebab-case events", function () {
this.server.respondWith("POST", "/test", function (xhr) {
var params = parseParams(xhr.requestBody);
xhr.respond(200, {}, params.foo);
});
var btn = make("<button hx-on='htmx:config-request: event.detail.parameters.foo = \"bar\"' hx-post='/test'>Foo</button>");
btn.click();
this.server.respond();
btn.innerText.should.equal("bar");
});
it("has the this symbol set to the element", function () {
this.server.respondWith("POST", "/test", function (xhr) {
xhr.respond(200, {}, "foo");
});
var btn = make("<button hx-on='htmx:config-request: window.elt = this' hx-post='/test'>Foo</button>");
btn.click();
this.server.respond();
btn.innerText.should.equal("foo");
btn.should.equal(window.elt);
delete window.elt;
});
it("can handle multi-line JSON", function () {
this.server.respondWith("POST", "/test", function (xhr) {
xhr.respond(200, {}, "foo");
});
var btn = make("<button hx-on='htmx:config-request: window.elt = {foo: true,\n" +
" bar: false}' hx-post='/test'>Foo</button>");
btn.click();
this.server.respond();
btn.innerText.should.equal("foo");
var obj = {foo: true, bar: false};
obj.should.deep.equal(window.elt);
delete window.elt;
});
it("can handle multiple event handlers in the presence of multi-line JSON", function () {
this.server.respondWith("POST", "/test", function (xhr) {
xhr.respond(200, {}, "foo");
});
var btn = make("<button hx-on='htmx:config-request: window.elt = {foo: true,\n" +
" bar: false}\n" +
" htmx:afterRequest: window.foo = true'" +
" hx-post='/test'>Foo</button>");
btn.click();
this.server.respond();
btn.innerText.should.equal("foo");
var obj = {foo: true, bar: false};
obj.should.deep.equal(window.elt);
delete window.elt;
window.foo.should.equal(true);
delete window.foo;
});
});

View File

@@ -506,7 +506,7 @@ describe("Core htmx Events", function() {
it("preventDefault() in htmx:configRequest stops the request", function () {
try {
var handler = htmx.on("htmx:configRequest", function (evt) {
evt.detail.errors.push("An error");
evt.preventDefault();
});
var request = false;
this.server.respondWith("POST", "/test", function (xhr) {

View File

@@ -66,6 +66,7 @@
<script src="attributes/hx-include.js"></script>
<script src="attributes/hx-indicator.js"></script>
<script src="attributes/hx-disinherit.js"></script>
<script src="attributes/hx-on.js"></script>
<script src="attributes/hx-params.js"></script>
<script src="attributes/hx-patch.js"></script>
<script src="attributes/hx-post.js"></script>

View File

@@ -92,6 +92,8 @@ Autorespond: <input id="autorespond" type="checkbox" onclick="toggleAutoRespond(
<a id="a1" hx-boost="true" href="#" target="">Asdf</a>
</div>
<button onclick="console.log(this, event)">Log It...</button>
<script>
let requestCount = 0;
this.server.respondWith("GET", "/demo", function(xhr){

53
www/attributes/hx-on.md Normal file
View File

@@ -0,0 +1,53 @@
---
layout: layout.njk
title: </> htmx - hx-on
---
## `hx-on`
The `hx-on` attribute allows you embed JavaScript scripts to respond to events directly on an element. It is
very similar to the [`onevent` properties](https://developer.mozilla.org/en-US/docs/Web/Events/Event_handlers#using_onevent_properties)
found in HTML, such as `onClick`.
`hx-on` improves on the `onevent` handlers in that it can handle any events, not just a fixed number of specific
DOM events. This allows you to respond to, for example, the many htmx-emitted events in a nice, embedded manner
that gives good [Locality of Behavior (LoB)](/essays/locality-of-behavior).
The `hx-on` attribute's value is an event name, followed by a colon, followed by the event handler code:
```html
<div>
<button hx-get="/info" hx-on="htmx:beforeRequest: alert('Making a request!')">
Get Info!
</button>
</div>
```
Here the event `hmtx:beforeRequest` is captured and shows an alert. Note that it is not possible to respond to this
event using the `onevent` properties in normal HTML.
### Symbols
Following the conventions of the `onevent` properties, two symbols are available in the body of the event handler code:
* `this` - Set to the element on which the `hx-on` attribute is defined
* `event` - Set to the event that triggered the handler
### Multiple Handlers
Multiple handlers can be defined by putting them on new lines:
```html
<div>
<button hx-get="/info" hx-on="htmx:beforeRequest: alert('Making a request!')
htmx:afterRequest: alert('Done making a request!')">
Get Info!
</button>
</div>
```
### Notes
* `hx-on` is _not_ inherited, however due to
[event bubbling](https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Building_blocks/Events#event_bubbling_and_capture),
`hx-on` attributes on parent elements will typically be triggered by events on child elements

View File

@@ -37,7 +37,8 @@ customClasses: wide-content
* [extensions](#extensions)
* [events & logging](#events)
* [debugging](#debugging)
* [hyperscript](#hyperscript)
* [scripting](#scripting)
* [hyperscript](#hyperscript)
* [3rd party integration](#3rd-party)
* [caching](#caching)
* [security](#security)
@@ -1166,7 +1167,80 @@ Here is an example of the code in action:
```
## <a name="hyperscript"></a>[hyperscript](#hyperscript)
## <a name="scripting"></a>[Scripting](#scripting)
While htmx encourages a hypermedia-based approach to building web applications, it does not preclude scripting and
offers a few mechanisms for integrating scripting into your web application. Scripting was explicitly included in
the REST-ful description of the web architecture in the [Code-On-Demand](https://www.ics.uci.edu/~fielding/pubs/dissertation/rest_arch_style.htm#sec_5_1_7)
section. As much as is feasible, we recommend a [hypermedia-friendly](/essays/hypermedia-friendly-scripting) approach
to scripting in your htmx-based web application:
* [Respect HATEOAS](/essays/hypermedia-friendly-scripting#prime_directive)
* [Use events to communicate between components](/essays/hypermedia-friendly-scripting#events)
* [Use islands to isolate non-hypermedia components from the rest of your application](/essays/hypermedia-friendly-scripting#islands)
* [Consider inline scripting](/essays/hypermedia-friendly-scripting#inline)
The primary integration point between htmx and scripting solutions is the [events](#events) that htmx sends and can
respond to. See the SortableJS example in the [3rd Party Javascript](3rd-party) section for a good template for
integrating a JavaScript library with htmx via events.
Scripting solutions that pair well with htmx include:
* [VanillaJS](http://vanilla-js.com/) - Simply using the built-in abilities of JavaScript to hook in event handlers to
respond to the events htmx emits can work very well for scripting. This is an extremely lightweight and increasingly
popular approach.
* [AlpineJS](https://alpinejs.dev/) - Alpine.js provides a rich set of tools for creating sophisticated front end scripts,
including reactive programming support, while still remaining extremely lightweight. Alpine encourages the "inline scripting"
approach that we feel pairs well with htmx.
* [jQuery](https://jquery.com/) - Despite its age and reputation in some circles, jQuery pairs very well with htmx, particularly
in older code-bases that already have a lot of jQuery in them.
* [hyperscript](https://hyperscript.org) - Hyperscript is an experimental front-end scripting language created by the same
team that created htmx. It is designed to embed well in HTML and both respond to and create events, and pairs very well
with htmx.
### <a name="hx-on"></a>[The `hx-on` Attribute](#hyperscript)
HTML allows the embedding of inline scripts via the [`onevent` properties](https://developer.mozilla.org/en-US/docs/Web/Events/Event_handlers#using_onevent_properties),
such as `onClick`:
```html
<button onclick="alert('You clicked me!')">
Click Me!
</button>
```
This feature allows scripting logic to be co-located with the HTML elements the logic applies to, giving good
[Locality of Behavior (LoB)](/essays/locality-of-behavior). Unfortunately, HTML only allows `on*` attributes for a fixed
number of specific DOM events (e.g. `onclick`) and doesn't offer a way to respond generally to events in this embedded
manner.
In order to address this shortcoming, htmx offers the [`hx-on`](/attributes/hx-on) attribute. This attribute allows
you to respond to any event in a manner that preserves the LoB of the `on*` properties:
```html
<button hx-on="click: alert('You clicked me!')">
Click Me!
</button>
```
For a `click` event, we would recommend sticking with the standard `onclick` attribute. However, consider an htmx-powered
button that wishes to add an attribute to a request using the `htmx:configRequest` event. This would not be possible
with an `on*` property, but can be done using the `hx-on` attribute:
```html
<button hx-post="/example"
hx-on="htmx:beforeRequest: event.detail.parameters.example = 'Hello Scripting!'">
Post Me!
</button>
```
Here the `example` parameter is added to the `POST` request before it is issued, with the value 'Hello Scripting!'.
The `hx-on` attribute is a very simple mechanism for generalized embedded scripting. It is _not_ a replacement for more
fully developed front-end scripting solutions such as AlpineJS or hyperscript. It can, however, augment a VanillaJS-based
approach to scripting in your htmx-powered application.
### <a name="hyperscript"></a>[hyperscript](#hyperscript)
Hyperscript is an experimental front end scripting language designed to be expressive and easily embeddable directly in HTML
for handling custom events, etc. The language is inspired by [HyperTalk](http://hypercard.org/HyperTalk%20Reference%202.4.pdf),

View File

@@ -56,6 +56,7 @@ The table below lists all other attributes available in htmx.
| [`hx-history-elt`](/attributes/hx-history-elt) | the element to snapshot and restore during history navigation
| [`hx-include`](/attributes/hx-include) | include additional data in requests
| [`hx-indicator`](/attributes/hx-indicator) | the element to put the `htmx-request` class on during the request
| [`hx-on`](/attributes/hx-on) | defines event handlers inline on an element
| [`hx-params`](/attributes/hx-params) | filters the parameters that will be submitted with a request
| [`hx-patch`](/attributes/hx-patch) | issues a `PATCH` to the specified URL
| [`hx-preserve`](/attributes/hx-preserve) | specifies elements to keep unchanged between requests