Vue Turbolinks Hotwire Adapter
vue-turbolinks is a JavaScript package designed to facilitate the integration of Vue.js components into applications that utilize Turbolinks or Hotwire's Turbo. Its primary function is to manage the lifecycle events of Vue components—specifically their proper setup and teardown—within the context of a Turbolinks/Turbo page navigation cycle. This allows developers to incrementally add interactive Vue elements to traditional server-rendered applications without converting them into full Single-Page Applications (SPAs). The current stable version is 2.2.2, with recent minor updates indicating active maintenance. A key differentiator is its explicit focus on solving the challenges of combining Vue's reactivity with Turbolinks'/Turbo's caching mechanisms, making it unsuitable and unnecessary for projects already employing client-side routing libraries like Vue Router.
Common errors
-
NoModificationAllowedError: Failed to set the 'outerHTML' property on 'Element'
cause Attempting to destroy a Vue root component whose top-level element is a `<transition>` component during a Turbolinks/Turbo cache event.fixWrap your root `<transition>` component in a regular HTML element (e.g., `<div>`) in your Vue component's template. -
TypeError: Cannot read properties of undefined (reading 'use') when installing globally
cause Attempting to use `Vue.use(TurbolinksAdapter)` before Vue is properly imported or globally available.fixEnsure `import Vue from 'vue';` is present at the top of your file or that Vue is globally exposed before attempting to install the adapter globally. If using Webpack, check your configuration for Vue aliasing.
Warnings
- gotcha Do not use `vue-turbolinks` if your application already employs a client-side JavaScript routing library like `vue-router`. This adapter is designed for traditional server-rendered applications enhanced by Turbolinks or Hotwire's Turbo, not for Single-Page Applications (SPAs).
- gotcha When a `$root` Vue component's direct root node is a `<transition>` element, its destruction upon `turbo:before-cache` or `turbolinks:before-cache` can fail with `NoModificationAllowedError`. This is because directly setting `outerHTML` on a Vue transition component's root might not be supported or stable.
- breaking Prior to v2.2.1, the `turbolinksAdapterMixin` might not have been correctly exported, causing issues when attempting to import it as a named export.
Install
-
npm install vue-turbolinks -
yarn add vue-turbolinks -
pnpm add vue-turbolinks
Imports
- TurbolinksAdapter
const TurbolinksAdapter = require('vue-turbolinks');import TurbolinksAdapter from 'vue-turbolinks';
- turbolinksAdapterMixin
const { turbolinksAdapterMixin } = require('vue-turbolinks');import { turbolinksAdapterMixin } from 'vue-turbolinks'; - Options for Vue.use()
Vue.use(TurbolinksAdapter, { destroyEvent: 'turbo:before-cache' });
Quickstart
import { turbolinksAdapterMixin } from 'vue-turbolinks';
import Vue from 'vue';
// A mock App component for demonstration
const App = {
template: '<div>Hello from Vue and Turbolinks! Count: {{ count }} <button @click="count++">Increment</button></div>',
data() {
return { count: 0 };
},
mounted() {
console.log('App mounted!');
},
beforeDestroy() {
console.log('App beforeDestroy!');
}
};
document.addEventListener('turbo:load', () => {
const element = document.getElementById("hello");
if (element != null) {
new Vue({
el: element,
template: '<App/>',
mixins: [turbolinksAdapterMixin],
components: { App }
});
console.log('Vue app initialized on turbo:load.');
} else {
console.log('No #hello element found, skipping Vue initialization.');
}
});
// Simulate a Turbolinks/Turbo page load for demonstration
// In a real app, this would be triggered by navigation.
setTimeout(() => {
const rootDiv = document.createElement('div');
rootDiv.id = 'hello';
document.body.appendChild(rootDiv);
document.dispatchEvent(new Event('turbo:load'));
}, 500);
setTimeout(() => {
const elementToRemove = document.getElementById('hello');
if (elementToRemove) {
elementToRemove.parentNode.removeChild(elementToRemove);
}
document.dispatchEvent(new Event('turbo:before-cache')); // Simulate cache event
}, 3000);