Take a deep breath, hold…

In 4 short and easy commits you’ll get a Spring Boot app serving server-side rendered pages via Thymeleaf (with Bootstrap) and Vue.js scripts making use of Axios to make asynchronous requests to the server to update pages with data without reloading the whole page from the server again.

… release! Explaining the previous phrase may take longer than to implement it. So let’s do it.

Commit # 1 - Spring Initializr

This one is easy. The first commit is the base Spring Boot application from Spring Initializr + a few extras.

You can generate your own version downloading from this link (already populated with some dependencies).

Add the following to the pom.xml file after loading the project in your preferred IDE:

<dependency>
    <groupId>org.webjars</groupId>
    <artifactId>bootstrap</artifactId>
    <version>4.4.1</version>
</dependency>
<dependency>
    <groupId>org.webjars</groupId>
    <artifactId>webjars-locator</artifactId>
    <version>0.38</version>
</dependency>

Commit # 2 - The base app

This commit may be long but it’s not complicated at all. It adds Role and User entities:

@Entity(name = "role")
@Data
public class Role {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private long id;

    private String name;
}
@Entity(name = "user")
@Data
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private long id;

    private String firstName;
    private String lastName;

    @ManyToOne
    @JoinColumn(name = "role_id")
    private Role role;
}

And their corresponding Spring Data JPA repositories:

@Repository
public interface RoleRepository extends JpaRepository<Role, Long> {
}
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
}

Adds a controller to provide a server-side rendered page via Thymeleaf:

@Controller
public class MainController {

    @RequestMapping("/")
    public String index() {
        return "index";
    }
}

And the index.html page itself on the resources/templates folder:

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <link href='/webjars/bootstrap/css/bootstrap.min.css' rel='stylesheet'>

    <meta charset="UTF-8">
    <title>Home</title>
</head>
<body>

<div th:replace="fragments/header :: header"></div>

<script src="/webjars/jquery/jquery.min.js"></script>
<script src="/webjars/bootstrap/js/bootstrap.min.js"></script>
</body>
</html>

Did you notice the <div th:replace...? This is a neat feature of Thymeleaf to reuse code. Here it goes the reusable header.html on the resources/templates/fragments folder:

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <link href='/webjars/bootstrap/css/bootstrap.min.css' rel='stylesheet'>

    <meta charset="UTF-8">
    <title>Restaurants</title>
</head>
<body>

<nav class="navbar navbar-expand-lg navbar-dark bg-dark" th:fragment="header">
    <a class="navbar-brand" href="/">Home</a>
    <button aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation"
            class="navbar-toggler" data-target="# navbarSupportedContent" data-toggle="collapse" type="button">
        <span class="navbar-toggler-icon"></span>
    </button>

    <div class="collapse navbar-collapse" id="navbarSupportedContent">
        <ul class="navbar-nav mr-auto">
            <li class="nav-item dropdown">
                <a aria-expanded="false" aria-haspopup="true" class="nav-link dropdown-toggle" data-toggle="dropdown"
                   href="# " id="navbarDropdown" role="button">
                    Entities
                </a>
                <div aria-labelledby="navbarDropdown" class="dropdown-menu">
                    <a class="dropdown-item" href="/users">Users</a>
                    <a class="dropdown-item" href="/roles">Roles</a>
                    <div class="dropdown-divider"></div>
                    <div class="dropdown-item-text p-4 text-muted" style="max-width: 200px;">
                        <p>
                            Administrative pages to list, edit, create and remove entities.
                        </p>
                    </div>
                </div>
            </li>
        </ul>
        <form class="form-inline my-2 my-lg-0">
            <input aria-label="Search" class="form-control mr-sm-2" placeholder="Search" type="search">
            <button class="btn btn-outline-success my-2 my-sm-0" type="submit">Search</button>
        </form>
    </div>
</nav>

<script src="/webjars/jquery/jquery.min.js"></script>
<script src="/webjars/bootstrap/js/bootstrap.min.js"></script>
</body>
</html>

Commit # 3 - The HTML templates for the entities

Adds server-side-rendered templates and controllers. Templates provide basic html pages for Roles and Users.

This is the new RoleController:

@Controller
public class RoleController {

    @GetMapping("/roles")
    public String rolesPage() {
        return "roles";
    }
}

And the UserController:

@Controller
public class UserController {

    @GetMapping("/users")
    public String users() {
        return "users";
    }
}

The HTML for the roles.html page:

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <link href='/webjars/bootstrap/css/bootstrap.min.css' rel='stylesheet'>

    <meta charset="UTF-8">
    <title>Roles</title>
</head>
<body>

<div th:replace="fragments/header :: header"></div>

<br><br>

<div class="container" id="main">

    <table class="table table-striped table-bordered">
        <thead>
        <tr>
            <th>Role ID</th>
            <th>Role Name</th>
            <th>Actions</th>
        </tr>
        </thead>
        <tbody>
        <tr>
            <td></td>
            <td></td>
            <td>
                <a>Edit</a>
                <a>Delete</a>
            </td>
        </tr>
        </tbody>
    </table>
</div>

<script src="/webjars/jquery/jquery.min.js"></script>
<script src="/webjars/bootstrap/js/bootstrap.min.js"></script>
</body>
</html>

And users.html page:

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <link href='/webjars/bootstrap/css/bootstrap.min.css' rel='stylesheet'>

    <meta charset="UTF-8">
    <title>Users</title>
</head>
<body>

<div th:replace="fragments/header :: header"></div>

<br><br>

<div class="container" id="main">

    <table class="table table-striped table-bordered">
        <thead>
        <tr>
            <th>First Name</th>
            <th>Last Name</th>
            <th>Role</th>
            <th>Actions</th>
        </tr>
        </thead>
        <tbody>
        <tr>
            <td></td>
            <td></td>
            <td></td>
            <td>
                <a>Edit</a>
                <a>Delete</a>
            </td>
        </tr>
        </tbody>
    </table>
</div>

<script src="/webjars/jquery/jquery.min.js"></script>
<script src="/webjars/bootstrap/js/bootstrap.min.js"></script>
</body>
</html>

Commit # 4 - The Vue magic

Now we add Vue and Axios to the pom.xml file:

<dependency>
    <groupId>org.webjars</groupId>
    <artifactId>vue</artifactId>
    <version>2.6.11</version>
</dependency>
<dependency>
    <groupId>org.webjars.npm</groupId>
    <artifactId>axios</artifactId>
    <version>0.19.0</version>
</dependency>

Then a REST controller (this serializes the response to JSON by default) to return a list of Roles:

@RestController
@RequestMapping("/api/v1")
public class RolesController {

    private final RoleRepository roleRepository;

    public RolesController(RoleRepository roleRepository) {
        this.roleRepository = roleRepository;
    }

    @GetMapping("roles")
    public List<Role> list() {
        return roleRepository.findAll();
    }
}

And another one for Users:

@RestController
@RequestMapping("/api/v1")
public class UsersController {

    private final UserRepository userRepository;

    public UsersController(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    @GetMapping("/users")
    public List<User> list() {
        return userRepository.findAll();
    }
}

Finally, on the roles.html we edit the section where we want Vue.js to render the data and add a script:

<!-- unrelated code omitted for brevity -->
<tr v-for="role in roles">
    <td>{{ role.id }}</td>
    <td>{{ role.name }}</td>
<!-- unrelated code omitted for brevity -->
<!-- Vue.js imports -->
<script src="webjars/vue/vue.min.js"></script>
<script src="webjars/axios/dist/axios.min.js"></script>
<!-- Actual Vue.js script -->
<script>
    var app = new Vue({
        el: '# main',
        data() {
            return {
                roles: null
            }
        },
        mounted(){
            axios
                .get("/api/v1/roles")
                .then(response => (this.roles = response.data))
        },
    })
</script>

The same modifications are made on the users.html:

<!-- unrelated code omitted for brevity -->
<tr v-for="user in users">
    <td>{{ user.firstName }}</td>
    <td>{{ user.lastName }}</td>
    <td>{{ user.role.name }}</td>
<!-- unrelated code omitted for brevity -->
<!-- Vue.js imports -->
<script src="webjars/vue/vue.min.js"></script>
<script src="webjars/axios/dist/axios.min.js"></script>
<!-- Actual Vue.js script -->
<script>
    var app = new Vue({
        el: '# main',
        data() {
            return {
                users: null
            }
        },
        mounted(){
            axios
                .get("/api/v1/users")
                .then(response => (this.users = response.data))
        },
    })
</script>

The app

You can see the app running here (slightly modified since this is an ongoing analysis).

The repository and the aforementioned commits, also slightly modified, here.

AQAP Series

As Quickly As Possible (AQAP) is a series of quick posts on something I find interesting. I encourage (and take part on) the discussions on the comments to further explore the technology, library or code quickly explained here.

Beginners tag

I’m using this tag for the second time. Since there are rules to use it, please let me know if there’s something here you (or a beginner) don’t understand or that I took for granted and you’re (or a beginner may be :) confused about it.


Image by Jason King por Pixabay

This post is also available on DEV.