Fork me on GitHub

JavalinVue Plugin

The JavalinVue plugin provides a very clever integration with Vue.js. As with most clever programming tricks, you will probably either love it or hate it. These docs are only valid for the current version of Javalin (6.1.3).

How does it work?

The JavalinVue plugin is basically a very specialized templating engine. It finds .vue (and optionally .js and .css) files and glues them together, and serves it all as one big HTML file. You start by creating a layout file:

<head>
    <script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.min.js"></script>
    @componentRegistration <!-- JavalinVue will find required vue files and inline them here -->
</head>
<body>
<main id="main-vue" v-cloak>
    @routeComponent <!-- Your route component will be inlined here (app.get("/my-page", VueComponent("my-page"))) -->
</main>
<script>
    new Vue({el: "#main-vue"});
</script>
</body>
<head>
    <script src="https://cdn.jsdelivr.net/npm/vue@3/dist/vue.global.min.js"></script>
    <script>const app = Vue.createApp({});</script>
    @componentRegistration <!-- JavalinVue will find required vue files and inline them here -->
</head>
<body>
<main id="main-vue" v-cloak>
    @routeComponent <!-- Your route component will be inlined here (app.get("/my-page", VueComponent("my-page"))) -->
</main>
<script>
    app.mount("#main-vue");
</script>
</body>

When a user tries to access /my-page in their browser, JavalinVue will serve the following HTML:

<head>
    <script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.min.js"></script>
    <!-- <my-page></my-page> component and all of its dependencies -->
</head>
<body>
<main id="main-vue" v-cloak>
    <my-page></my-page> <!-- this was defined in app.get("/my-page", VueComponent("my-page")) -->
</main>
<script>
    new Vue({el: "#main-vue"});
</script>
</body>
<head>
    <script src="https://cdn.jsdelivr.net/npm/vue@3/dist/vue.global.min.js"></script>
    <script>const app = Vue.createApp({});</script>
    <!-- <my-page></my-page> component and all of its dependencies -->
</head>
<body>
<main id="main-vue" v-cloak>
    <my-page></my-page> <!-- this was defined in app.get("/my-page", VueComponent("my-page")) -->
</main>
<script>
    app.mount("#main-vue");
</script>
</body>

You don’t need any frontend build tool (like Webpack, Parcel, Grunt, etc) – JavalinVue takes care of all that. As a consequence import/export of ES modules is not needed. You can still use native ES modules if you want to, for example if you want to use a third-party library that is only available as an ES module.

There is a longer tutorial which includes the motivation behind creating this integration, as well as some discussion about pros and cons: /tutorials/simple-frontends-with-javalin-and-vue

Getting Started

Creating a layout

Create a root directory for your vue files and place your layout.html in it. By default, JavalinVue will look in src/main/resources/vue:

src
└───main
    └───resources
        └───vue
            └───layout.html

Your layout.html file will be responsible for initializing Vue and including all your dependencies. The snippet below shows all the available macros (@macroName), except @cdnWebjar:

<head>
    <script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.min.js"></script>
    <style>@inlineFile("/vue/styles.css")</style> <!-- always included -->
    <script>@inlineFileDev("/vue/scripts-dev.js")</script> <!-- only included in dev -->
    <script>@inlineFileNotDev("/vue/scripts-not-dev.js")</script> <!-- only included in not dev -->
    @componentRegistration <!-- JavalinVue will find required vue files and inline them here -->
</head>
<body>
<main id="main-vue" v-cloak>
    @routeComponent <!-- Your route component will be inlined here (app.get("/my-page", VueComponent("my-page"))) -->
</main>
<script>
    new Vue({el: "#main-vue"});
</script>
</body>
<head>
    <script src="https://cdn.jsdelivr.net/npm/vue@3/dist/vue.global.min.js"></script>
    <script>const app = Vue.createApp({});</script>
    <style>@inlineFile("/vue/styles.css")</style> <!-- always included -->
    <script>@inlineFileDev("/vue/scripts-dev.js")</script> <!-- only included in dev -->
    <script>@inlineFileNotDev("/vue/scripts-not-dev.js")</script> <!-- only included in not dev -->
    @componentRegistration <!-- JavalinVue will find required vue files and inline them here -->
</head>
<body>
<main id="main-vue" v-cloak>
    @routeComponent <!-- Your route component will be inlined here (app.get("/my-page", VueComponent("my-page"))) -->
</main>
<script>
    app.mount("#main-vue");
</script>
</body>

The @cdnWebjar macro is used to include a webjar when running in dev mode, see @cdnWebjar for more information.

Creating a component

Components will be inlined where the @componentRegistration macro is present in your layout.html, which means you have to register them as global Vue components:

<template id="my-component">
    <div>
        <!-- Component code goes here -->
    </div>
</template>
<script>
    Vue.component("my-component", {
        template: "#my-component"
    });
</script>
<template id="my-component">
    <div>
        <!-- Component code goes here -->
    </div>
</template>
<script>
    app.component("my-component", {
        template: "#my-component"
    });
</script>

This component will now be available to be called from any other component you make, or as a @routeComponent.

Binding to a route

Routing is done server side, so you bind a component to a route by declaring a GET endpoint in Javalin:

app.get("/my-path", new VueComponent("my-component"));
app.get("/my-path", VueComponent("my-component"))

This means that you can use the same AccessManager for frontend routes as you use for your API:

app.get("/my-path", new VueComponent("my-component"), roles(Role.LOGGED_IN));
app.get("/my-path", VueComponent("my-component"), roles(Role.LOGGED_IN))

Configuration

Javalin.create(config -> {
    config.vue.rootDirectory        // where JavalinVue should look for files (default: decided based on isDevFunction)
    config.vue.vueAppName           // the Vue 3 app name (default: null)
    config.vue.isDevFunction        // a function to determine if request is on localhost (default: checks ctx.url())
    config.vue.optimizeDependencies // only include required vue files (default: true)
    config.vue.stateFunction        // a function which runs on every request for transferring state from server (default: null)
    config.vue.cacheControl         // cache control header (default: "no-cache, no-store, must-revalidate")
    config.vue.enableCspAndNonces   // will enable csp and tag each component with a nonce (default: false)
});
Javalin.create { config ->
    config.vue.rootDirectory        // where JavalinVue should look for files (default: decided based on isDevFunction)
    config.vue.vueAppName           // the Vue 3 app name (default: null)
    config.vue.isDevFunction        // a function to determine if request is on localhost (default: checks ctx.url())
    config.vue.optimizeDependencies // only include required vue files (default: true)
    config.vue.stateFunction        // a function which runs on every request for transferring state from server (default: null)
    config.vue.cacheControl         // cache control header (default: "no-cache, no-store, must-revalidate")
    config.vue.enableCspAndNonces   // will enable csp and tag each component with a nonce (default: false)
}

rootDirectory

By default, JavalinVue will set the root directory based on the first request it serves.

This is done to make development fast locally, and requests fast in production. If you set the root dir explicitly, Javalin won’t try to guess what to do:

rootDirectory(Path path) // set location with explicit Path object
rootDirectory(String path) // set relative path (classpath by default)
rootDirectory(String path, Location location) // location can be CLASSPATH or EXTERNAL
rootDirectory(String path, Location location, Class resourcesJarClass) // add a class to specify which jar holds the resources
rootDirectory(path: Path) // set location with explicit Path object
rootDirectory(path: String) // set relative path (classpath by default)
rootDirectory(path: String, location: Location) // location can be CLASSPATH or EXTERNAL
rootDirectory(path: String, location: Location, resourcesJarClass: Class<*>) // add a class to specify which jar holds the resources

The resourcesJarClass becomes relevant if you are packing your application as multiple jars. If you are using a fat-jar/uber-jar, everything is flattened, and Javalin will find the resources automatically. If you are using something like distTar, you will need to point JavalinVue to the Jar which contains your resources. You can do this through rootDirectory(path = "/path", resourcesJarClass = MyClass::class.java).

vueAppName

This setting is only required if you are using Vue 3:

vueAppName = "MyAppName";
vueAppName = "MyAppName"

This setting should match how you initialize your Vue instance: const MyAppName = Vue.createApp({});

isDevFunction

By default, this isDevFunction is set to check if the request host is "localhost" or "127.0.0.1". This function is called once on the first request JavalinVue sees, and is used to set an isDev property, which is then used to make decisions on how to build the HTML for a request.

isDevFunction = ctx -> // your code here
isDevFunction { /* Your code here */ }

optimizeDependencies

By default, this is set to true. If you set it to false, every .vue file that JavalinVue finds will be inlined in @componentRegistration. If you leave it as true, only required .vue files will be included.

optimizeDependencies = true/false;
optimizeDependencies = true/false

stateFunction

If you want to share state from your server with Vue, you can provide JavalinVue with a state function:

stateFunction = ctx -> Map.of("user", getUser(ctx));
stateFunction = { mapOf("user" to getUser(it)) }

This can then be accessed from the state variable:

<template id="user-template">
    <div>{{ $javalin.state.user }}</div>
</template>

The function runs for every request, so the state is always up to date when the user navigates or refreshes the page.

cacheControl

By default, JavalinVue sets the "Cache-Control" header to "no-cache, no-store, must-revalidate". This can be configured:

cacheControl = "cache header string";
cacheControl = "cache header string"

enableCspAndNonces

This will make Javalin generate a nonce for every script tag in your app, as well as set the CSP header.

enableCspAndNonces = true/false;
enableCspAndNonces = true/false

Layout macros

@inlineFile("/path/to/file.ext")        // this file will always be inlined
@inlineFileDev("/path/to/file.ext")     // this file will be inlined if JavalinVue.isDev is true
@inlineFileNotDev("/path/to/file.ext")  // this file will be inlined if JavalinVue.isDev is false
@componentRegistration                  // all required components will be inlined here
@routeComponent                         // the current route component will be inlined here
@cdnWebjar                              // will resolve to webjar path if dev, cdn if not dev

@cdnWebjar

You can reference your WebJars with @cdnWebjar/ instead of the normal /webjars/. If you do this, the path will resolve to /webjars/ on when isDevFunction returns true, and https//cdn.jsdelivr.net/.../ on non-localhost. Note that this only works with NPM webjars.

LoadableData

The JavalinVue plugin includes a small class for making HTTP get-requests to your backend, it can be used like this:

<template id="books-component">
    <div>
        <div v-if="books.loading">Loading books ...</div>
        <div v-if="books.loadError">Failed to load books! ({{books.loadError.text}})</div>
        <div v-if="books.loaded" v-for="book in books.data">{{book}}</div>
    </div>
</template>
<script>
    Vue.component("books-component", {
        template: "#books-component",
        data: () => ({
            books: new LoadableData("/api/books"),
        }),
    });
</script>
<template id="books-component">
    <div>
        <div v-if="books.loading">Loading books ...</div>
        <div v-if="books.loadError">Failed to load books! ({{books.loadError.text}})</div>
        <div v-if="books.loaded" v-for="book in books.data">{{book}}</div>
    </div>
</template>
<script>
    app.component("books-component", {
        template: "#books-component",
        data: () => ({
            books: new LoadableData("/api/books"),
        }),
    });
</script>

The class automatically caches the request in localStorage, so subsequent requests will appear to load instantly. All configuration options and methods are shown below:

const useCache = true/false;
const errorCallback = error => alert(`An error occurred! Code: ${error.code}, text: ${error.text}`);

// Create a new instance with config options
const loadableData = new LoadableData("/api/books", useCache, errorCallback);
// Refresh data (can use cache to avoid flickering)
loadableData.refresh(useCache, errorCallback);

// Refresh data for other instances which uses the same endpoint
let users = new LoadableData("/users");
let sameUsers = new LoadableData("/users"); // this variable could be in a different component
users.refreshAll(); // sameUsers will also be refreshed, since they share the same URL

// Refresh data via static method
LoadableData.refreshAll("/users"); // all instances with this URL will refresh themselves

The loadError object contains the HTTP status and error message, and is available in both the template and in the error callback function.

FAQ

File not found

Good to know

JavalinVue will also put path-parameters in the Vue instance, which you can access like this:

<template id="thread-view">
    <div>{{ $javalin.pathParams["user"] }}</div>
</template>
<script>
    Vue.component("thread-view", {
        template: "#thread-view"
    });
</script>

Local state

This feature came to life because someone was abusing JavalinVue.stateFunction by overwriting it for every request. If you find yourself doing this, you should either rewrite your app to fetch data using LoadableData (recommended), or use local state for VueComponent (usually not recommended).

Every VueComponent can take a state object as a second parameter. This state overwrites the state from stateFunction. You can either add it directly in a route declaration:

app.get("/specific-state", VueComponent("test-component", mapOf("test" to "tast")))

Or inside a handler:

app.get("/specific-state") { ctx ->
    val myState = mapOf("test" to "tast")
    VueComponent("test-component", myState).handle(ctx)
}

Note that this is something that you CAN do it, not something that you SHOULD do. Only do this if you have thought hard about it, and it solves a real problem for you.

Like Javalin?
Star us 😊

×