Javalin: Vue router and javalin-vue

Created on 22 Oct 2020  路  14Comments  路  Source: tipsy/javalin

After a month, and two apps later my first experience of Javalin, I ran into a problem I can't figure out.

I used JavalinVue to support my dashboard page.
I have a dashboard with a fixed top navbar and an also fixed (for the sake of the example) and always visible right sidebar.
When I open the dashboard, it displays as it should.

image

However, when I click on the brand (navigating to /ownerhome) or the menu item (navigating to /services), it doesn't display (replace the router-view) to the given component. Additionally, while the navbar remains, the sidebar disappears.

image

What I would like to achieve: keep the top navbar and the sidebar (don't reload them, because that's costly) in spot and replace only the content.
I tried a lot of options, but can't find the reason. The console shows that the router.beforeEach is called and the url is changed.

layout.html

<html>
<head>
    <title>[[Application.name]]</title>
    <meta charset="utf8">
    <script src="/webjars/vue/2.6.10/dist/vue.min.js"></script>

    <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Titillium+Web">

    <link rel="stylesheet"
          href="https://maxst.icons8.com/vue-static/landings/line-awesome/line-awesome/1.3.0/css/line-awesome.min.css">

    <link rel="shortcut icon" href="/favicon.png" type="image/x-icon"/>

    <link type="text/css" rel="stylesheet"
          href="https://stackpath.bootstrapcdn.com/bootswatch/4.5.2/cyborg/bootstrap.min.css"/>
    <script src="https://unpkg.com/[email protected]/dist/bootstrap-vue.min.js"></script>
    <script src="https://unpkg.com/vue-router/dist/vue-router.js"></script>
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-vue/2.18.0/bootstrap-vue.min.css"
          integrity="sha512-oqSSCKWeQcVs+9Hek1tZlj8hMlLhDNI+NUmm56nBCsO4OLE8dyDpaUA/b2zGi+M9SML0xwGtk22ozH2qyqKipg=="
          crossorigin="anonymous"/>

    @componentRegistration
</head>
<body>
<main id="main-vue" v-cloak>
    @routeComponent
</main>
<script>
    const EventBus = new Vue({})

    Vue.config.devtools = true

    const routes = [
        {
            name: 'home',
            path: '/home',
            component: ownerhome
        },
        {
            name: 'services',
            path: '/services',
            component: ownerservices
        },
    ]

    const router = new VueRouter({
        routes 
    })

    router.beforeEach(async (to, from, next) => {
        console.log(to, next)
        next()
    })

    const vue = new Vue({router}).$mount("#main-vue");
</script>
</body>
</html>

It has nothing surprising: the only special is to add VueRouter support.

dashboard.vue

<template id="dashboard">
    <div >
        <app-frame>
            <router-view></router-view>
        </app-frame>
    </div>
</template>
<script>
Vue.component("dashboard", {
    template: "#dashboard"
});


</script>
<style>
</style>

app-frame.vue

<template id="app-frame">
    <v-app>
        <v-content>
            <ownernavbar></ownernavbar>
            <glossarysidebar></glossarysidebar>
            <v-container>
                <slot></slot>
            </v-container>
        </v-content>
    </v-app>
</template>
<script>
Vue.component("app-frame", {template: "#app-frame"});
</script>
<style>
</style>

This two files are straightforward.

home.vue

<template id="ownerhome">
    <v-content class="bg-info">
        <b-card title="test" tag="article" style="max-width: 20rem;">
            <b-card-text>
                Text
            </b-card-text>

            <b-button href="#" variant="primary">Go somewhere</b-button>
        </b-card>
    </v-content>
</template>
<script>
Vue.component("ownerhome", {
    template: "#ownerhome"
})
</script>
<style>
</style>

I feel I miss some nuance, but can't find it. :-(

QUESTION

Most helpful comment

Thanks for the answers. They will most probably make my SPA concept needless.

This optimizeDependencies option is great and I'll check it.
The stateFunction is also a great alternative for my problem. It is almost identical to handling client side state. Almost, because it has some differences: it contains only data, so if I need to make some post process or would like decorate the pure data with some utility functions, it still has to do with each reload. But this I could live with.
A few questions about the use of this function: as I saw in the source (I am away and could only browse the source code of Javalin, but can't try these features) that this function produces Any and this value is serialized to JSON. Could I inject a pure JSON into the state as is? My server side already has it's JSON serialization (not pure GSON, they build up the JSON by themself), so I would like to use a stateFunction producing JSON. Is it possible?

Also, I will try to figure out why the flickering happens, because it is clear now that the window is cleared long before the page is displayed. I have a guess, but has to check it.

All 14 comments

Also, I feel routing in layout.html is somehow misplaced, due to there may be other pages (like login) using the same wrapper. So my question if I could add routing in dashboard.vue?

@balage1551 half of the point of JavalinVue is to provide server-side routing rather than client-side, so you would do

    get("/home", VueComponent("home"))
    get("/dashboard", VueComponent("dashboard"))

Could you explain what you're using VueRouter for ?

Could you explain what you're using VueRouter for ?

Yes, By routing on server side, the whole page is rendered again each time we switch page.
Apart from the fact it is time consuming, cause flickering (having a dark theme page, the flickering is even worse) while rendering the page (Vue is quite time consuming on client side, needing 1-2 sec to handle a complex page), the biggest problem is I lost all state. The state of the two static parts (navbar and sidebar) has to be fetched again and recalculated.

The main advantage of SPA is to have static state of the static components and to refresh the content part of the page only.

Of course, I could give up all the JavalinVue features and move to the common SPA solution and doing all the things separately, but I have a mixed app: there is the login page and a SPA for each user roles. Also I enjoy the way how I don't have to move "out" of IDEA environment by using Javalin's Vue great integration solution.

That's why I try to use router to handle the per-role SPA without flicker.

PS: I use server side routing for the separated pages and I started the whole app without vue router, but it turned out to have the problems I listed above.

Of course, I could "emulate" the router by defining a data flag indicating the page and adding all the components with v-if selector based on this flag, but I thought why should I emulate something I could do by a tool.

Apart from the fact it is time consuming, cause flickering (having a dark theme page, the flickering is even worse)

If you structure your layout-file and components in a certain way, this won't happen. Most browsers don't unload the current page until the new one is ready now, and since components are loaded in <head> they will be parsed before the page is ready. This means you can can avoid flickering for all static html (such as the app frame).

while rendering the page (Vue is quite time consuming on client side, needing 1-2 sec to handle a complex page),

This sounds a bit slow. You can try the new JavalinVue.optimizeDependencies = true, which will only include the required dependencies of a VueComponent (if you are relying on JavalinVue to include pure CSS stored in .vue files, you will have to use <style>@inlineFile("/vue/styles.css")</style> to include these now.

the biggest problem is I lost all state. The state of the two static parts (navbar and sidebar) has to be fetched again and recalculated.

This can be solved by using JavalinVue.stateFunction = { ctx -> ... } (global, applies to all components), or the more secret VueComponent("dashboard", state") (local to the component).

just to also add my 2C, you can also change the caching policy for production for views, by setting

JavalinVue.cacheControl = "max-age=259200,must-revalidate";//cache files for 3 days

you can also do a bit of UX optimization, by isolating your main theme colors and blocks into its own theme.css and loading that as the first file in your head tag and having that cached as well.

If you want something that is a huge SPA with complex views, you might be better off using Static files and creating a normal npm project based on Vue js and having a postbuild script that moves them into an appropriate java resources folder.

Though when using JavalinVue.optimizeDependncies = true, you can knock down pages to be into single digit kilobytes

You can also hack in an HTML compressor using an app.after middleware to knock it down even further, but that takes CPU on the server-side

Thanks for the answers. They will most probably make my SPA concept needless.

This optimizeDependencies option is great and I'll check it.
The stateFunction is also a great alternative for my problem. It is almost identical to handling client side state. Almost, because it has some differences: it contains only data, so if I need to make some post process or would like decorate the pure data with some utility functions, it still has to do with each reload. But this I could live with.
A few questions about the use of this function: as I saw in the source (I am away and could only browse the source code of Javalin, but can't try these features) that this function produces Any and this value is serialized to JSON. Could I inject a pure JSON into the state as is? My server side already has it's JSON serialization (not pure GSON, they build up the JSON by themself), so I would like to use a stateFunction producing JSON. Is it possible?

Also, I will try to figure out why the flickering happens, because it is clear now that the window is cleared long before the page is displayed. I have a guess, but has to check it.

Thanks for the answers. They will most probably make my SPA concept needless.

That's the idea 馃槃

so I would like to use a stateFunction producing JSON. Is it possible?

I guess you could give it a map("json", yourJson), in which case you would access it as $javalin.state.json ?

Also, I will try to figure out why the flickering happens

Let me know if you have any questions!

I guess you could give it a map("json", yourJson)

I'm back home and tested the proposed solution, but it doesn't work.
My first problem is that I use (in the other part of the project GSon's Json implementation, instead of Jackson). But the real problem is, that if you add anything to the state (as a Map entry as you mentioned) the JavalinVue tries to serialize the entry.

I'm not at home with Jackson, but I have a proposal for greater flexibility: provide a way to handle state data as a raw string (holding meaningful JS data without further serialization) and add to the page source as is. To avoid breaking any existing code, we can't use the String itself, but I propose a wrapper:

interface RawState {
    val rawState : String
}

class SimpleRawState(override val rawState : String) : RawState
class MutableRawState(override var rawState: String ) : RawState

And then modify the state injection in JavalinVue to:

    internal fun getState(ctx: Context, state: Any?) = "\n<script>\n" + """
        |    Vue.prototype.${"$"}javalin = {
        |        pathParams: ${JavalinJson.toJson(ctx.pathParamMap().mapKeys { escape(it.key) }.mapValues { escape(it.value) })},
        |        queryParams: ${JavalinJson.toJson(ctx.queryParamMap().mapKeys { escape(it.key) }.mapValues { it.value.map { escape(it) } })},
        |        state: ${toRawState(ctx,state)}
        |    }""".trimMargin() + "\n</script>\n"

    private fun toRawState(ctx: Context, state : Any?) : String {
        val s =  state ?: stateFunction(ctx)
        return if (s is RawState) s.rawState else JavalinJson.toJson(s) 
    }

This would give great flexibility to produce the state where the GUI integration is done after the core is developed (which is a common pattern) and already having it's own serialization solution. What do you think?

_PS: While my proposal is discussed and probably implemented, I use a workaround by adding raw string (converting my server side json into a string and sending the string to the client side, where I parse it and build up the JSON structure again, but it needs two needless conversion._

My first problem is that I use (in the other part of the project GSon's Json implementation, instead of Jackson).

JavalinJson can use GSON:

Gson gson = new GsonBuilder().create();
JavalinJson.setFromJsonMapper(gson::fromJson);
JavalinJson.setToJsonMapper(gson::toJson);

What do you think?

To be honest, it sounds odd to me. I imagine the state would come from the application itself (some sort of Kotlin state object), not a prebuilt foreign string. Where does this string come from?

If you do mapOf("json" to """{"a": "1", "b": "2"}"""), you can do JSON.parse($javalin.state.json)["a"] on the client side and get out "1". You can save this as a computed property for easy access.

To be honest, it sounds odd to me. I imagine the state would come from the application itself (some sort of Kotlin state object), not a prebuilt foreign string. Where does this string come from?

The state is strongly related to the GUI so it is built up and maintained by the Server, not by the business objects. It comes from projections of several different classes, represented in a form it is best for the front-end. Also, several of my classes (mostly configuration ones) already has their JSON serialization solution for other purposes. This is not a GSON auto serialization, but a simple json DSL production (this makes the projection much easier). Although -- as you suggest and as the current implementation provides -- I could make dedicated state (data) classes representing the state JSON, fill them and then let Javalin serialize them, but these classes would have the sole existence to be the base of a JSON serialization, so this step may be omitted by creating the JSON itself. (BTW, to do this JSON creation, I use a quite boilercode-free, easy to read JSON builder DSL.)

The general goal behind the state function, Javalin offers is to pass a state to the front-end as a JSON. Javalin should not bother how this JSON is produced (as an extreme example, it may come form static JSON file, as well) Do not force the user how he produce this JSON (by JSON serialization, at the moment), but let the JSON (as string) to be injected in any way. The serialization should be kept as a utility, convenience function.

What I propose is to let the developer decide how the state should be generated, and passed to the front-end. At the moment, the mechanism forces the user to use either an unstructured map or a JSON serializable, just-for-this-function objects. My extension may offer -- beside the current JSON serialization option -- an alternative way to produce and pass the state as the user thinks the best.

If you do mapOf("json" to """{"a": "1", "b": "2"}"""), you can do JSON.parse($javalin.state.json)["a"] on the client side and get out "1". You can save this as a computed property for easy access.

As for there is no other way at the moment, I do almost this. The difference is I don't parse the JSON each time it is accessed (as your suggestion), but do it once, in layout.html:

    Vue.prototype.$javalin.parsedState = JSON.parse(Vue.prototype.$javalin.state)

I can live with it, but feel a little silly to serialize a JSON into a string to have been deserialize on client side.

The general goal behind the state function, Javalin offers is to pass a state to the front-end as a JSON. Javalin should not bother how this JSON is produced (as an extreme example, it may come form static JSON file, as well) Do not force the user how he produce this JSON (by JSON serialization, at the moment), but let the JSON (as string) to be injected in any way. The serialization should be kept as a utility, convenience function.

In principle I agree with this, I guess I'm just being difficult because I've never encountered this problem myself.
Feel free to go ahead with a PR :)

@balage1551 I guess we can close this now, but feel free to create a PR to address your other issue.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

vikascn picture vikascn  路  4Comments

TobiasWalle picture TobiasWalle  路  3Comments

spinscale picture spinscale  路  3Comments

valtterip picture valtterip  路  5Comments

davioooh picture davioooh  路  3Comments