bigskysoftware / htmx

</> htmx - high power tools for HTML
https://htmx.org
Other
38.06k stars 1.29k forks source link

Toward writing a HEAD element management extension #247

Closed deanebarker closed 3 years ago

deanebarker commented 3 years ago

Opening from #245, since that issue was resolved and closed.

This issue is concerned with this note from @1cg:

That said, what you have looks like an excellent start to a head extension. Please let me know if you are interested in pursuing that, I'd be happy to help out.

Specifically, I'm interested in an extension/framework to alter the HEAD tag when boosting the entire page. When using boosting, the BODY swaps, but only the TITLE changes in the HEAD (if supplied by the response). I would like to create a way to manage elements in the HEAD that are specific to the new page.

As mentioned in #245, this model seems to work:

htmx.on("htmx:beforeSwap", function(evt) {

    var incomingDOM = new DOMParser().parseFromString(evt.detail.xhr.response, "text/html");           

    var path = "head *[data-page-specific='true']";
    document.querySelectorAll(path).forEach(function(e) {
        e.parentNode.removeChild(e);
    });
    incomingDOM.querySelectorAll(path).forEach(function(e) {
        document.head.appendChild(e);
    })

});

Any incoming element with an attribute of data-page-specific will...

  1. be removed from the existing DOM
  2. be added from the incoming DOM

I have this running on a product site, and it is working. Some META is perpetual from page-to-page, but anything specific to the incoming page like og:title gets a data-page-specific attribute. Those elements will get removed on the next load, and the new page specific elements will be added.

(Note: I'll likely change the attribute to something more inline with convention, like ht-boost-swap.)

One question for @1cg or @bencroker before I start writing something --

Is there any official way to detect a boost reload? I actually had to surround the above code with this...

if(evt.target.nodeName == "BODY")
{
  [above code here]
}

...otherwise it executed on every htmx request.

Is there a cleaner/more official way for me to determine that this is an entire page reload rather than a partial?

bencroker commented 3 years ago

What if instead of adding a new attribute, we explored making meta tags work similar to how the title tag currently works.

So if a page contains:

<html>
<head>
  <title>Original Title</title>
  <meta name="description" content="Original description">
  <meta name="keywords" content="my, key, words">
</head>

And the response contains:

<title>New Title</title>
<meta name="description" content="New description">
<meta name="keywords" content="">
<meta name="author" content="J.D.">

Then the result becomes:

<html>
<head>
  <title>New Title</title>
  <meta name="description" content="New description">
  <meta name="keywords" content="">
  <meta name="author" content="J.D.">
</head>
deanebarker commented 3 years ago

That would be lovely, but there are other page-specific things:

So, it's not just META

bencroker commented 3 years ago

All of those tags can go in the body, except for "head-only" link tags (including canonical), unfortunately. https://html.spec.whatwg.org/multipage/links.html#body-ok

deanebarker commented 3 years ago

Well, now we're talking about adding something to the core of the boosting code in htmx, which is far beyond the scope of an extension.

1cg commented 3 years ago

Thanks @deanebarker for opening this issue up. My opinion is that this should start off as an extension and then potentially be folded into the core of htmx if it is small enough and unobtrusive enough to warrant it. It should look specifically for content in a <head> tag and update the head tag.

Ideally this would be a merge, like morphdom, in order to minimize the amount of reflow (my understanding is that updating the head tag causes reflows).

Meta tags look pretty easy since name appears to be unique. Script tags in a head are a bit tricker, but probably not terrible. I would do the dumb thing first (look at the src attribute or body of the tag and compare w/ existing). I assume all the other possibilities are roughly the same.

As far as where to plug in, I think a new event is needed, hmtx:afterResponseParse, roughly here:

https://github.com/bigskysoftware/htmx/blob/fda4957d653ebf683d6017c07a6ef34ed2d72983/src/htmx.js#L137

Additionally, I think we need to include the settleInfo in the event, because my sense is that we probably want to do at least some of the processing after the DOM has settled.

@deanebarker the way htmx swaps work in general is as follows:

This sounds kind of crazy, but it lets you use CSS transitions without resorting to javascript. Consider the following HTML

  <div id="div1">Original Content</div>

if content comes down from the server that looks like this:

  <div id="div1" class="red">New Content</div>

It will be inserted into the DOM like this:

  <div id="div1">New Content</div>

(note, no class attribute.) And then, after a delay (100ms by default) it will be changed to the complete new value:

  <div id="div1" class="red">New Content</div>

That gives you an opportunity to write a CSS transition to, for example, fade the new content to red:

.red {
  color: red;
  transition: all ease-in 1s ;
}

All in pure HTML.

@deanebarker I can start a new branch feature-head-ext to work on this with you, if you'd like.

deanebarker commented 3 years ago

Should you add the new event first? I think we should get that added and merged, because that really stands alone -- it might be used for things beyond this, right? I think it's cleaner if we don't mix that change up with this.

1cg commented 3 years ago

OK, I have a new branch for this work, with the framework for an extension and the new even added:

https://github.com/bigskysoftware/htmx/commit/532681fd028b24ca8a9b02aef800edfe8efc60bc

deanebarker commented 3 years ago

My prior solution was brute force -- get rid of anything with the attribute in the existing DOM, and add anything with the attribute in the incoming DOM.

Does this still sound like the right plan, except we'll check for existence first, in both directions? So, before we remove, we'll check to see if that same thing is in the incoming DOM, and if it is, we leave it alone (don't remove). And in the other direction, we'll check to see if the element we're about to add is already in the existing DOM, and if it is, we don't add it.

But this is still driven by the attribute, right? No attribute, then we don't anything with it? Or are we just checking everything now?

1cg commented 3 years ago

I think we'll need to do per-tag logic:

We'll probably have to think through each tag. :)

deanebarker commented 3 years ago

for a meta tag we can just check the name

property too, sadly. The two attributes are used interchangeably.

deanebarker commented 3 years ago

Okay, so this where I level with you, @1cg: I'm not very good with JavaScript. I barely fumble through it. I'm not really even a developer anymore (this is me), but my past experience is in C#.

I classically know just enough about JavaScript to hurt myself.

So, I will stumble through this, with the understanding that you're probably going to have to come behind me and make it all not suck. But I can do the rote legwork to hopefully figure out the logic of it.

On the plus side, I'm in the state next door to you.

Great plains, FTW. Go Bobcats.

1cg commented 3 years ago

All good, I wrote htmx to avoid writing javascript, so I get it. :)

Just take a crack at it and me and @bencroker can help out where you have trouble.

The tests are probably the place to start, understanding how they work.

deanebarker commented 3 years ago

I've been looking through all the permutations of tags in the HEAD that would contribute to "unique-ness," and it's lot. There are very simple ones like META and name or property, but then there's some weird ones, like the hreflang attribute on LINK, etc.

There's a long tail.

I think we're going to need to have some way to specify what to check. If you look back on #245, I had some code like this:

swapTags("meta[name='twitter:title']", doc);
swapTags("meta[name='twitter:image']", doc);

I was specifying the tags I wanted to check. We might need to do something similar -- have the most common ones specified as DOM paths, and perhaps give the user some ability to add to it, if they have something more esoteric they want to check.

1cg commented 3 years ago

What does a wholesale .innerHTML replacement do?

I have to be honest, my reflow-foo isn't great, so I don't even know how to tell what the implications are.

deanebarker commented 3 years ago

Given that we're replacing the entire BODY too, I can't see how also replacing the entire innards of the HEAD tag is going to have a material impact. We're essentially kicking off the "ultimate reflow operation" anyway...

1cg commented 3 years ago

closing out due to lost interest, we will have an open branch on this if interest revives