[beginners, vue, axios, spring]


You'll have a better experience reading in DEV

Click here to continue reading this post there >>

However, if you want to know more about the project to mirror my posts from DEV here (and why), go ahead and read more.

You can continue to read here too, it's up to you... =]


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>
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode
@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;
}
Enter fullscreen mode Exit fullscreen mode

And their corresponding Spring Data JPA repositories:

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

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

@Controller
public class MainController {

    @RequestMapping("/")
    public String index() {
        return "index";
    }
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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";
    }
}
Enter fullscreen mode Exit fullscreen mode

And the UserController:

@Controller
public class UserController {

    @GetMapping("/users")
    public String users() {
        return "users";
    }
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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();
    }
}
Enter fullscreen mode Exit fullscreen mode

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();
    }
}
Enter fullscreen mode Exit fullscreen mode

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></td>
    <td></td>
<!-- unrelated code omitted for brevity -->
Enter fullscreen mode Exit fullscreen mode
<!-- 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>
Enter fullscreen mode Exit fullscreen mode

The same modifications are made on the users.html:

<!-- unrelated code omitted for brevity -->
<tr v-for="user in users">
    <td></td>
    <td></td>
    <td></td>
<!-- unrelated code omitted for brevity -->
Enter fullscreen mode Exit fullscreen mode
<!-- 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>
Enter fullscreen mode Exit fullscreen mode

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.

GitHub logo brunodrugowick / spring-thymeleaf-vue-crud-example

Complete CRUD example project with Spring Boot, Thymeleaf, Vue.js and Axios.

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