Using HTMX inside Vue
Published: 2023-10-17 09:38:16

#### Why would you do this to yourself ####

Mixing frontend technologies is a recipe for disaster unless there is a clear division of responsibilities. Here the issue will obviously be mixing fundamentally different state handling. But if you don't really care about the state of particular content, it's possible to simplify the client-side app by offloading parts of the app to htmx. For example listing purchase history or something like that can be entirely separated from the dynamic part of the page.

Ideally htmx and Vue shouldn't need to know about each other's existence. This is simple if htmx is used outside Vue:

```
<div id="app"></div>

<div>
... htmx stuff
</div>

```

Naturally it's easy to add htmx to Vue templates but the issue is htmx can't register event handlers if elements are added outside the library after initial page load. This would be disastrous because the page would be visually fine but none of the events would work properly. Luckily htmx API already has a solution:

> process(): Processes the given element and its children, hooking up any htmx behavior

Adding some custom mutation tracker or state sharing to Vue for this is simply out of the question. However we can create a wrapper that simply runs htmx.process() on it's content when it is mounted to DOM, basically creating a sandbox for htmx. Ideally the parent component simply does not care about what htmx is doing. I would argue that at most Vue should pass some initial parameters if absolutely necessary. 

```
<template>
    <div v-if="error">
        <p>oops...better do smth</p>
    </div>

    <div ref="htmxcontent">
        <slot></slot>
    </div>
</template>

<script setup lang="ts">
import { ref, onMounted } from "vue";
const error = ref(false);
const htmxcontent = ref(null);

// Add htmx events to elements after they have been attached to DOM
onMounted(() => {

    if (typeof (window as any).htmx?.process !== 'function') {
        error.value = true;
        // ...
        return;

    }

    try {
        (window as any).htmx.process(htmxcontent.value);
    } catch (e) {
        error.value = true;
        //....
    }
});
</script>


```

This is simple and works. It's probably a good idea to check htmx is loaded or the user might get a broken page without visual indication. Of course this is unlikely but could happen. ( regardless of using Vue) Anyway, now it's possible to just drop htmx inside vue template like this:


```
<HtmxWrapper>
    <div>
        <button hx-get="/history" hx-swap="innerHTML">View history</button>
    </div>
</HtmxWrapper>

```

The caveat here is that the component is trashed if it is unmounted due to some condition, navigation or other reason. If this is not acceptable, it can be nested inside keep-alive block so this "htmx box" state persists even after remount. Since nodes are only detached, not destroyed, they also maintain their event listeners. Probably it is better to write htmx templates in separate files and just import them during build. There are many ways to achieve this, using Vite you can simply:

```
import htmxSnippet from "@/htmx/order-history.html?raw";

<template>
  <KeepAlive>
    <HtmxWrapper>
      <div v-html="htmxSnippet"></div>
    </HtmxWrapper>
  </KeepAlive>
```

***Should I do it?***

I don't know. Try and see how it works. You're not really committing to anything by using htmx. Surely writing simple forms and such things as components is easy but why add them if it's not necessary? There is no need to do data fetching, parsing,error handling or rendering on client. Also there will be a few less reactive elements to track for runtime.


Why this might be useful:
+ Basic templating can be done on server
+ Reducing component internal state
+ htmx API is very convenient and lightweight
+ Functionality can be added thru htmx later without touching FE

Why this might be risky:
+ Eventually you will try Vue <--> htmx interactions and sharing state
+ broken page with no visible feedback
+ all the issues not discovered yet