Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 8 additions & 8 deletions src/editors/jetbrains/htmx.web-types.json
Original file line number Diff line number Diff line change
Expand Up @@ -621,24 +621,24 @@
"doc-url": "https://four.htmx.org/extensions/hx-download#events"
},
{
"name": "before:head:merge",
"name": "head:before:merge",
"description": "Fires before the hx-head extension processes response `<head>` content. `detail.ctx` is the request context.",
"doc-url": "https://four.htmx.org/extensions/hx-head#htmxbeforeheadmerge"
"doc-url": "https://four.htmx.org/extensions/hx-head#htmxheadbeforemerge"
},
{
"name": "before:head:add",
"name": "head:before:add",
"description": "Fires before the hx-head extension adds a new head element. `detail.headElement` is the element. Cancel to skip it.",
"doc-url": "https://four.htmx.org/extensions/hx-head#htmxbeforeheadadd"
"doc-url": "https://four.htmx.org/extensions/hx-head#htmxheadbeforeadd"
},
{
"name": "before:head:remove",
"name": "head:before:remove",
"description": "Fires before the hx-head extension removes a head element. `detail.headElement` is the element. Cancel to keep it.",
"doc-url": "https://four.htmx.org/extensions/hx-head#htmxbeforeheadremove"
"doc-url": "https://four.htmx.org/extensions/hx-head#htmxheadbeforeremove"
},
{
"name": "after:head:merge",
"name": "head:after:merge",
"description": "Fires after the hx-head extension merges head content. `detail.added`, `detail.kept`, and `detail.removed` list affected elements.",
"doc-url": "https://four.htmx.org/extensions/hx-head#htmxafterheadmerge"
"doc-url": "https://four.htmx.org/extensions/hx-head#htmxheadaftermerge"
},
{
"name": "history:cache:before:save",
Expand Down
10 changes: 5 additions & 5 deletions src/ext/hx-head.js
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@
}
} else {
// if this is a merge, we remove this content since it is not in the new head
if (htmx.trigger(document.body, "htmx:before:head:remove", {headElement: currentHeadElt}) !== false) {
if (htmx.trigger(document.body, "htmx:head:before:remove", {headElement: currentHeadElt}) !== false) {
removed.push(currentHeadElt)
}
}
Expand All @@ -124,7 +124,7 @@
newNode._preloadHint = hint
}
} else {
if (htmx.trigger(document.body, "htmx:before:head:add", {headElement: newNode}) !== false) {
if (htmx.trigger(document.body, "htmx:head:before:add", {headElement: newNode}) !== false) {
await appendNode(newNode)
added.push(newNode)
}
Expand All @@ -134,12 +134,12 @@
// remove all removed elements, after we have appended the new elements to avoid
// additional network requests for things like style sheets
for (const removedElement of removed) {
if (htmx.trigger(document.body, "htmx:before:head:remove", {headElement: removedElement}) !== false) {
if (htmx.trigger(document.body, "htmx:head:before:remove", {headElement: removedElement}) !== false) {
currentHead.removeChild(removedElement)
}
}

htmx.trigger(document.body, "htmx:after:head:merge", {
htmx.trigger(document.body, "htmx:head:after:merge", {
added: added,
kept: preserved,
removed: removed
Expand All @@ -160,7 +160,7 @@
let target = ctx.target
// TODO - is there a better way to handle this? it used to be based on if the element was boosted
let defaultMergeStrategy = target === document.body ? "merge" : "append";
if (htmx.trigger(document.body, "htmx:before:head:merge", detail)) {
if (htmx.trigger(document.body, "htmx:head:before:merge", detail)) {
let realText = ctx.response.raw.text.bind(ctx.response.raw)
ctx.response.raw.text = async () => {
let text = await realText()
Expand Down
24 changes: 12 additions & 12 deletions test/tests/ext/hx-head.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ describe('hx-head extension', function() {

// Wait for head merge to complete
async function afterMerge() {
await waitForEvent('htmx:after:head:merge', 500)
await waitForEvent('htmx:head:after:merge', 500)
.catch(() => {}); // no head = no event, that's fine
await forRequest();
}
Expand Down Expand Up @@ -109,34 +109,34 @@ describe('hx-head extension', function() {
assert.isNotNull(added, 'new element should be added');
});

it('fires htmx:before:head:merge and htmx:after:head:merge events', async function() {
it('fires htmx:head:before:merge and htmx:head:after:merge events', async function() {
let beforeFired = false;
let afterFired = false;

let onBefore = () => { beforeFired = true; };
let onAfter = () => { afterFired = true; };
document.body.addEventListener('htmx:before:head:merge', onBefore);
document.body.addEventListener('htmx:after:head:merge', onAfter);
document.body.addEventListener('htmx:head:before:merge', onBefore);
document.body.addEventListener('htmx:head:after:merge', onAfter);

mockResponse('GET', '/page', headResponse('<meta name="hx-head-test-evt" content="val">', '<div>content</div>'));
let div = createProcessedHTML('<div hx-get="/page" hx-swap="innerHTML">click</div>');

div.click();
await afterMerge();

document.body.removeEventListener('htmx:before:head:merge', onBefore);
document.body.removeEventListener('htmx:after:head:merge', onAfter);
document.body.removeEventListener('htmx:head:before:merge', onBefore);
document.body.removeEventListener('htmx:head:after:merge', onAfter);

let added = document.head.querySelector('meta[name="hx-head-test-evt"]');
if (added) addedHeadElts.push(added);

assert.isTrue(beforeFired, 'htmx:before:head:merge should fire');
assert.isTrue(afterFired, 'htmx:after:head:merge should fire');
assert.isTrue(beforeFired, 'htmx:head:before:merge should fire');
assert.isTrue(afterFired, 'htmx:head:after:merge should fire');
});

it('htmx:before:head:merge cancellation prevents merge', async function() {
it('htmx:head:before:merge cancellation prevents merge', async function() {
let onBefore = (e) => { e.preventDefault(); };
document.body.addEventListener('htmx:before:head:merge', onBefore, {once: true});
document.body.addEventListener('htmx:head:before:merge', onBefore, {once: true});

mockResponse('GET', '/page', headResponse('<meta name="hx-head-test-cancel" content="val">', '<div>content</div>'));
let div = createProcessedHTML('<div hx-get="/page" hx-swap="innerHTML">click</div>');
Expand Down Expand Up @@ -181,7 +181,7 @@ describe('hx-head extension', function() {

let onMerge = () => { headMergeTime = Date.now(); };
let onSwap = () => { swapTime = Date.now(); };
document.body.addEventListener('htmx:after:head:merge', onMerge);
document.body.addEventListener('htmx:head:after:merge', onMerge);
document.body.addEventListener('htmx:after:swap', onSwap);

mockResponse('GET', '/page', headResponse('<meta name="hx-head-test-timing" content="val">', '<div>swapped</div>'));
Expand All @@ -190,7 +190,7 @@ describe('hx-head extension', function() {
div.click();
await afterMerge();

document.body.removeEventListener('htmx:after:head:merge', onMerge);
document.body.removeEventListener('htmx:head:after:merge', onMerge);
document.body.removeEventListener('htmx:after:swap', onSwap);

let added = document.head.querySelector('meta[name="hx-head-test-timing"]');
Expand Down
29 changes: 21 additions & 8 deletions www/src/content/extensions/04-hx-head.md
Original file line number Diff line number Diff line change
Expand Up @@ -183,24 +183,24 @@ Element events expose:
event.detail.headElement // Element being added or removed
```

### `htmx:before:head:merge`
### `htmx:head:before:merge`

Fires before the extension processes a response `<head>`.

```js
document.addEventListener('htmx:before:head:merge', event => {
document.addEventListener('htmx:head:before:merge', event => {
if (!shouldUpdateHead(event.detail.ctx)) event.preventDefault()
})
```

The event detail includes the request `ctx`. Cancel the event to skip all head processing for the response.

### `htmx:before:head:add`
### `htmx:head:before:add`

Fires before the extension adds a head element.

```js
document.addEventListener('htmx:before:head:add', event => {
document.addEventListener('htmx:head:before:add', event => {
if (event.detail.headElement.matches('script[data-untrusted]')) {
event.preventDefault()
}
Expand All @@ -209,12 +209,12 @@ document.addEventListener('htmx:before:head:add', event => {

Cancel the event to skip that element.

### `htmx:before:head:remove`
### `htmx:head:before:remove`

Fires before the extension removes a current `<head>` element.

```js
document.addEventListener('htmx:before:head:remove', event => {
document.addEventListener('htmx:head:before:remove', event => {
if (event.detail.headElement.matches('[data-keep]')) {
event.preventDefault()
}
Expand All @@ -223,7 +223,7 @@ document.addEventListener('htmx:before:head:remove', event => {

Cancel the event to keep that element.

### `htmx:after:head:merge`
### `htmx:head:after:merge`

Fires after the extension finishes updating the `<head>`.

Expand All @@ -236,11 +236,24 @@ event.detail = {
```

```js
document.addEventListener('htmx:after:head:merge', event => {
document.addEventListener('htmx:head:after:merge', event => {
console.log('Head elements added:', event.detail.added.length)
})
```

## Migration

### Beta to RC1

RC1 namespaces `hx-head` lifecycle events:

| Beta | RC1 |
|------|-----|
| `htmx:before:head:merge` | [`htmx:head:before:merge`](#htmxheadbeforemerge) |
| `htmx:before:head:add` | [`htmx:head:before:add`](#htmxheadbeforeadd) |
| `htmx:before:head:remove` | [`htmx:head:before:remove`](#htmxheadbeforeremove) |
| `htmx:after:head:merge` | [`htmx:head:after:merge`](#htmxheadaftermerge) |

## Notes

- `<style>` applies and `<script>` runs before swap; `<script defer>` runs after:
Expand Down
Loading