DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Please enter at least three characters to search
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Zones

Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks

Last call! Secure your stack and shape the future! Help dev teams across the globe navigate their software supply chain security challenges.

Modernize your data layer. Learn how to design cloud-native database architectures to meet the evolving demands of AI and GenAI workloads.

Releasing software shouldn't be stressful or risky. Learn how to leverage progressive delivery techniques to ensure safer deployments.

Avoid machine learning mistakes and boost model performance! Discover key ML patterns, anti-patterns, data strategies, and more.

Related

  • Leveraging Salesforce Using a Client Written In Vue.js
  • Going Full-Stack With Kotlin/JS and Spring Boot
  • Spring Boot and React in Harmony
  • React, Angular, and Vue.js: What’s the Technical Difference?

Trending

  • Docker Base Images Demystified: A Practical Guide
  • AI Meets Vector Databases: Redefining Data Retrieval in the Age of Intelligence
  • The Modern Data Stack Is Overrated — Here’s What Works
  • Accelerating AI Inference With TensorRT
  1. DZone
  2. Coding
  3. JavaScript
  4. Augmenting the Client With Vue.js

Augmenting the Client With Vue.js

In this post, the author takes their first steps in augmenting an SSR app with Vue. In the next post, they will implement the same features with Alpine.js.

By 
Nicolas Fränkel user avatar
Nicolas Fränkel
DZone Core CORE ·
Sep. 26, 24 · Tutorial
Likes (1)
Comment
Save
Tweet
Share
4.5K Views

Join the DZone community and get the full member experience.

Join For Free

In my previous post, I laid the ground to build upon. Now is the time to start "for real".

I heard a lot of Vue.js. Additionally, a friend who transitioned from developer to manager told me good things about Vue, which further piqued my interest. I decided to take a look at it. It will be the first "lightweight" JavaScript framework I'll study — from the point of view of a newbie, which I am.

Laying out the Work

I explained WebJars and Thymeleaf in the last post. Here's the setup, server- and client-side.

Server-Side

Here is how I integrate both in the POM:

XML
 
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>       <!--1-->
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-thymeleaf</artifactId> <!--2-->
    </dependency>
    <dependency>
        <groupId>org.webjars</groupId>
        <artifactId>webjars-locator</artifactId>               <!--3-->
        <version>0.52</version>
    </dependency>
    <dependency>
        <groupId>org.webjars.npm</groupId>
        <artifactId>vue</artifactId>                           <!--4-->
        <version>3.4.34</version>
    </dependency>
</dependencies>


  1. Spring Boot itself; I decided on the regular, non-reactive approach
  2. Spring Boot Thymeleaf integration
  3. WebJars locator, to avoid specifying the Vue version on the client-side
  4. Vue, finally!

I'm using the Kotlin Router and Bean DSLs on the Spring Boot side:

Kotlin
 
fun vue(todos: List<Todo>) = router {                                    //1
    GET("/vue") {
        ok().render("vue", mapOf("title" to "Vue.js", "todos" to todos)) //2-3
    }
}


  1. Pass a static list of Todo objects
  2. See below
  3. Pass the model to Thymeleaf

If you're used to developing APIs, you're familiar with the body() function; it returns the payload directly, probably in JSON format. The render() passes the flow to the view technology, in this case, Thymeleaf. It accepts two parameters:

  1. The view's name. By default, the path is /templates and the prefix is .html; in this case, Thymeleaf expects a view at /templates/vue.html
  2. A model map of key-value pairs

Client-Side

Here's the code on the HTML side:

HTML
 
<script th:src="@{/webjars/axios/dist/axios.js}" src="https://cdn.jsdelivr.net/npm/axios@1.7/dist/axios.min.js"></script> <!--1-->
<script th:src="@{/webjars/vue/dist/vue.global.js}" src="https://cdn.jsdelivr.net/npm/vue@3/dist/vue.global.js"></script> <!--2-->
<script th:src="@{/vue.js}" src="../static/vue.js"></script>             <!--3-->
<script th:inline="javascript">
/*<![CDATA[*/
    window.vueData = {                                                   <!--4-->
        title: /*[[${ title }]]*/ 'A Title',
        todos: /*[[${ todos }]]*/ [{ 'id': 1, 'label': 'Take out the trash', 'completed': false }]
    };
/*]]>*/
</script>


  1. Axios helps making HTTP requests
  2. Vue itself
  3. Our client-side code
  4. Set the data

As explained in last week's article, one of Thymeleaf's benefits is that it allows both static file rendering and server-side rendering. To make the magic work, I specify a client-side path, i.e., src, and a server-side path, i.e., th:src.

A Title

The Vue Code

Now, let's dive into the Vue code.

We want to implement several features:

  1. After the page load, the page should display all Todo items
  2. When clicking on a Todo completed checkbox, it should set/unset the completed attribute
  3. When clicking on the Cleanup button, it deletes all completed Todo
  4. When clicking on the Add button, it should add a Todo to the list of Todowith the following values:
    • id: Server-side computed ID as the max of all other IDs plus 1
    • label: value of the Label field for label
    • completed: set to false

Our First Steps Into Vue

The first step is to bootstrap the framework. We have already set up the reference for our custom vue.js file above.

JavaScript
 
document.addEventListener('DOMContentLoaded', () => {                    //1
  // The next JavaScript code snippets will be inside the block
}


  1. Run the block when the DOM has finished loading

The next step is to let Vue manage part of the page. On the HTML side, we must decide which top-level part Vue manages. We can choose an arbitrary <div> and change it later if need be.

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


On the JavaScript side, we create an app, passing the CSS selector of the previous HTML <div>.

JavaScript
 
Vue.createApp({}).mount('#app');


At this point, we launch Vue when the page loads, but nothing visible happens.

The next step is to create a Vue template. A Vue template is a regular HTML `` managed by Vue. You can define Vue in Javascript, but I prefer to do it on the HTML page.

Let's start with a root template that can display the title.

HTML
 
<template id="todos-app">                                                <!--1-->
  <h1>{{ title }}</h1>                                                   <!--2-->
</template>


  1. Set the ID for easy binding
  2. Use the title property; it remains to be set up

On the JavaScript side, we must create the managing code.

JavaScript
 
const TodosApp = {
    props: ['title'],                                                    //1
    template: document.getElementById('todos-app').innerHTML,
}


  1. Declare the title property, the one used in the HTML template

Finally, we must pass this object when we create the app:

JavaScript
 
Vue.createApp({
    components: { TodosApp },                                            //1
    render() {                                                           //2
        return Vue.h(TodosApp, {                                         //3
            title: window.vueData.title,                                 //4
        })
    }
}).mount('#app');


  1. Configure the component
  2. Vue expects the render() function
  3. h() for hyperscript creates a virtual node out of the object and its properties
  4. Initialize the title property with the value generated server-side

At this point, Vue displays the title.

Basic Interactions

At this point, we can implement the action when the user clicks on a checkbox: it needs to be updated in the server-side state.

First, I added a new nested Vue template for the table that displays the Todo. To avoid lengthening the post, I'll avoid describing it in detail. If you're interested, have a look at the source code.

Here's the starting line template's code, respectively JavaScript and HTML:

JavaScript
 
const TodoLine = {
    props: ['todo'],
    template: document.getElementById('todo-line').innerHTML
}


HTML
 
<template id="todo-line">
    <tr>
        <td>{{ todo.id }}</td>                                           <!--1-->
        <td>{{ todo.label }}</td>                                        <!--2-->
        <td>
            <label>
                <input type="checkbox" :checked="todo.completed" />
            </label>
        </td>
    </tr>
</template>


  1. Display the Todo id
  2. Display the Todo label
  3. Check the box if its completed attribute is true

Vue allows event handling via the @ syntax.

HTML
 
<input type="checkbox" :checked="todo.completed" @click="check" />


Vue calls the template's check() function when the user clicks on the line. We define this function in a setup() parameter:

JavaScript
 
const TodoLine = {
    props: ['todo'],
    template: document.getElementById('todo-line').innerHTML,
    setup(props) {                                                                 //1
        const check = function (event) {                                           //2
            const { todo } = props
            axios.patch(                                                           //3
                `/api/todo/${todo.id}`,                                            //4
                { checked: event.target.checked }                                  //5
            )
        }
        return { check }                                                           //6
    }
}


  1. Accept the props array, so we can later access it
  2. Vue passes the event that triggered the call
  3. Axios is a JavaScript lib that simplifies HTTP calls
  4. The server-side must provide an API; it's outside the scope of this post, but feel free to check the source code.
  5. JSON payload
  6. We return all defined functions to make them accessible from HTML

Client-Side Model

In the previous section, I made two mistakes:

  • I didn't manage any local model
  • I didn't use the HTTP response's call method

We will do that by implementing the next feature, which is the cleanup of completed tasks.

We now know how to handle events via Vue:

HTML
 
<button class="btn btn-warning" @click="cleanup">Cleanup</button>


On the TodosApp object, we add a function of the same name:

JavaScript
 
const TodosApp = {
    props: ['title', 'todos'],
    components: { TodoLine },
    template: document.getElementById('todos-app').innerHTML,
    setup() {
        const cleanup = function() {                                               //1
            axios.delete('/api/todo:cleanup').then(response => {                   //1
                state.value.todos = response.data                                  //2-3
            })
        }
        return { cleanup }                                                         //1
    }
}


  1. As above
  2. Axios offers automated JSON conversion of the HTTP call
  3. state is where we store the model

In Vue's semantics, the Vue model is a wrapper around data that we want to be reactive. Reactive means two-way binding between the view and the model. We can make an existing value reactive by passing it to the ref() method:

In Composition API, the recommended way to declare reactive state is using the ref() function.

ref() takes the argument and returns it wrapped within a ref object with a .value property.

To access refs in a component's template, declare and return them from a component's setup() function.

— Declaring Reactive State

Let's do it:

JavaScript
 
const state = ref({
    title: window.vueData.title,                                         //1-2
    todos: window.vueData.todos,                                         //1
})

createApp({
    components: { TodosApp },
    setup() {
        return { ...state.value }                                        //3-4
    },
    render() {
        return h(TodosApp, {
            todos: state.value.todos,                                    //5
            title: state.value.title,                                    //5
        })
    }
}).mount('#app');


  1. Get the data set in the HTML page, via Thymeleaf, as explained above
  2. We change the way we set the title. It's not necessary since there's no two-way binding - we don't update the title client-side, but I prefer to keep the handling coherent across all values
  3. Return the refs, as per Vue's expectations
  4. Look, ma, I'm using the JavaScript spread operator
  5. Configure the object's attributed from the state

At this point, we have a reactive client-side model.

On the HTML side, we use the relevant Vue attributes:

HTML
 
<tbody>
  <tr is="vue:todo-line" v-for="todo in todos" :key="todo.id" :todo="todo"></tr> <!--1-2-->
</tbody>


  1. Loop over the list of Todo objects
  2. The is attribute is crucial to cope with the way the browser parses HTML. See Vue documentation for more details

I've described the corresponding template above.

Updating the Model

We can now implement a new feature: add a new Todo from the client. When clicking on the Add button, we read the Label field value, send the data to the API, and refresh the model with the response.

Here's the updated code:

JavaScript
 
const TodosApp = {
    props: ['title', 'todos'],
    components: { TodoLine },
    template: document.getElementById('todos-app').innerHTML,
    setup() {
        const label = ref('')                                            //1
        const create = function() {                                      //2
            axios.post('/api/todo', { label: label.value }).then(response => {
                state.value.todos.push(response.data)                    //3
            }).then(() => {
                label.value = ''                                         //4
            })
        }
        const cleanup = function() {
            axios.delete('/api/todo:cleanup').then(response => {
                state.value.todos = response.data                        //5
            })
        }
        return { label, create, cleanup }
    }
}


  1. Create a reactive wrapper around the title whose scope is limited to the function
  2. The create() function proper
  3. Append the new JSON object returned by the API call to the list of Todo
  4. Reset the field's value
  5. Replace the whole list when deleting; the mechanism is the same

On the HTML side, we add a button and bind to the create() function. Likewise, we add the Label field and bind it to the model.

HTML
 
<form>
    <div class="form-group row">
        <label for="new-todo-label" class="col-auto col-form-label">New task</label>
        <div class="col-10">
            <input type="text" id="new-todo-label" placeholder="Label" class="form-control" v-model="label" /> <!--1-2-->
        </div>
        <div class="col-auto">
            <button type="button" class="btn btn-success" @click="create">Add</button> <!--3-->
        </div>
    </div>
</form>


Vue binds the create() function to the HTML button. It does call it asynchronously and refreshes the reactive Todo list with the new item returned by the call. We do the same for the Cleanup button, to remove checked Todo objects.

Note that I didn't intentionally implement any error-handling code to avoid making the code more complex than necessary. I'll stop here as we gained enough insights for a first experience.

Conclusion

In this post, I took my first steps in augmenting an SSR app with Vue. It was pretty straightforward. The biggest issue I encountered was for Vue to replace the line template: I didn't read the documentation extensively and missed the is attribute.

However, I had to write quite a few lines of JavaScript, though I used Axios to help me with HTTP calls and didn't manage errors.

In the next post, I'll implement the same features with Alpine.js.

The complete source code for this post can be found on GitHub.

Go Further

  • Vue.js
JavaScript Vue.js Spring Boot

Published at DZone with permission of Nicolas Fränkel, DZone MVB. See the original article here.

Opinions expressed by DZone contributors are their own.

Related

  • Leveraging Salesforce Using a Client Written In Vue.js
  • Going Full-Stack With Kotlin/JS and Spring Boot
  • Spring Boot and React in Harmony
  • React, Angular, and Vue.js: What’s the Technical Difference?

Partner Resources

×

Comments
Oops! Something Went Wrong

The likes didn't load as expected. Please refresh the page and try again.

ABOUT US

  • About DZone
  • Support and feedback
  • Community research
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 100
  • Nashville, TN 37211
  • support@dzone.com

Let's be friends:

Likes
There are no likes...yet! 👀
Be the first to like this post!
It looks like you're not logged in.
Sign in to see who liked this post!