Background

I would like to share my experience using Vue 2 without a build step in my project and the key factors behind this approach. I started experimenting with Vue.js (Vue 2) back in 2020. One of the nice features of Vue is the ability to use the framework directly into HTML page without a build tool (compilation). This particular feature was the deciding factor in selecting this framework in migrating a jQuery-based web application. I also considered how easy it is to learn and train my team.

The project is a multiple page application (MPA), it consists of an HTML part (UI), an inline JavaScript with embedded Vue (UI Handler) and external Vue components.

imagen

A Spring Boot page controller handles the HTTP requests, webpages generation, session management and connection to the business services. An API controller handles the data flow between the web server to the web browser via REST API.

imagen

Frontend Framework Selection Criteria

Features / Capabilities Vue React Angular
No Build Step
Server Side Rendering
Learning curve
Reuse or retrofit existing jQuery components

No Build Step

The tasks were to upgrade an existing Spring 4 to Spring Boot 2, replace jQuery with Vue, provide bug fixes and add new business functionalities. It is important that we don’t introduce additional build tools into the development process and focus on the actual tasks of migrating the code and improving the business logic. Angular needed a build step to transpile TypeScript to JavaScript, while Vue and React could be used without a build step, but the latter required coding in JSX.

The sample code below shows how we use Vue directly with HTML and inline Javascript. Of course, we can also separate the Vue code into its own dedicated JavaScript file.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
<!DOCTYPE HTML>
<html xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
      xmlns:th="http://www.thymeleaf.org" xmlns:v-on="http://www.w3.org/1999/xhtml"
      layout:decorate="~{fragments/_layout}">
<head>
    <title>Catalog | ABC Company</title
</head>
<body>
<main class="main" layout:fragment="content">
    <div class="container-fluid">
        <div class="row" v-show="errors.length > 0" style="display: none;">
            <div class="col-12">
                <div id="error-alert" class="alert alert-danger alert-dismissible fade show" role="alert">
                    <strong>Error</strong>
                    <ul>
                        <li v-for="item in errors"><span v-html="item"></span></li>
                    </ul>
                    <button type="button" class="close" data-dismiss="alert" aria-label="Close">
                        <span aria-hidden="true">&times;</span>
                    </button>
                </div>
            </div>
        </div>
        <div class="row">
            <div class="col-12">
                <h3 class="mb-3">Production Dashboard</h3>
            </div>
        </div>
        <div class="row">
            <div class="col-12">
                <div class="card"
                <div class="card-body bg-light">
                    <div class="container-fluid">
                        <div class="row">
                            <div class="col-12 pt-2 pb-1 pl-0">
                                <button class="btn btn-sm btn-primary"
                                        @click="viewOrder"
                                        :disabled="disableContextButton">View
                                </button>
                                <button class="btn btn-sm btn-primary"
                                        @click="updateOrderQuantity"
                                        :disabled="disableContextButton">Update Order Quantity
                                </button>
                                <button class="btn btn-sm btn-primary"
                                        @click="startOrder"
                                        :disabled="disableContextButton">Start Order
                                </button>
                                <button class="btn btn-sm btn-primary"
                                        @click="refresh">Refresh
                                </button>
                            </div>
                        </div>
                        <div class="row">
                            <div class="col-12 p-0">
                                <div :style="{ height: gcHeight + 'px'}">
                                    <custom-vue-grid id="orderGrid" :schema="schema"
                                                     :columns="columns"
                                                     :page-size="pageSize" :height="gcHeight"
                                                     :exportable="true"
                                                     @on-row-selected="onSelectedHandler"
                                                     :username="loggedUsername">
                                    </custom-vue-grid>
                                </div>
                            </div>
                        </div>
                    </div>
                </div>
            </div>
        </div>
    </div>
</main>
<th:block layout:fragment="script">
    <script type="text/javascript" th:inline="javascript">
        let app = new Vue({
            el: "#root",
            data: {
                loggedUsername: /*[[${#authentication.name}]]*/ "",
                errors: [],
                gcHeight: 600,
                pageSize: 1000,
                selectedOrders: [],
                schema: {
                    model: {
                        fields: {
                            orderDateTime: {
                                type: "date"
                            },
                            deliveryDateTime: {
                                type: "date"
                            },
                        }
                    }
                },
                columns: [
                    {
                        hidden: true,
                        field: "idOrder",
                        title: "OrderID",
                        width: 100,
                        filterable: false
                    },
                    {
                        hidden: false,
                        field: "orderReference",
                        title: "Order No.",
                        width: 150
                    },
                    {
                        field: "orderType",
                        title: "Order Type",
                        width: 250
                    },
                    {
                        field: "customerName",
                        title: "Customer Name",
                        width: 300
                    },
                    {
                        field: "orderDateTime",
                        title: "Order Date/Time",
                        format: "{0:dd/MM/yyyy HH:mm}",
                        width: 210
                    },
                    {
                        field: "deliveryDateTime",
                        title: "delivery Date/Time",
                        format: "{0:dd/MM/yyyy HH:mm}",
                        width: 210
                    },
                    {
                        field: "status",
                        title: "Status",
                        width: 350
                    }
                ],
            },
            computed: {
                disableContextButton() {
                    try {
                        if (this.selectedOrders !== null && this.selectedOrders.length > 0) {
                            return false;
                        }
                    } catch (err) {
                        return true;
                    }
                    return true;
                },
            },
            methods: {
                onSelectedHandler(v) {
                    if (v !== null) {
                        this.selectedOrders = v;
                    }
                },
                startOrder() {
                    try {
                        window.location = '/start-order?id=' + this.selectedOrders[0].idOrder;
                        window.location = '/start-order?id=' + this.selectedOrders[0].idOrder;
                    } catch (err) {
                        console.log(err);
                        window.location.reload(true);
                    }
                },
                viewOrder() {
                    try {
                        if (this.selectedOrders.length > 0) {
                            window.open('/order-viewer/' + this.selectedOrders[0].idOrder, '_blank');
                        }
                    } catch (err) {
                        console.log(err);
                        window.location.reload(true);
                    }
                },
                loadOrders() {
                    sharedEvents.$emit('load-remote-data', {
                        id: 'orderGrid',
                        url: '/api/v1/get-orders'
                    });
                },
            },
            mounted() {
                //#region THIS SET THE GRID CONTAINER HEIGHT
                let heightFactor = 0.75;
                this.gcHeight = window.outerHeight * heightFactor;
                //#endregion
                this.loadOrders();
            }
        });
    </script>
</th:block>
</body>
</html>

Server Side Rendering

The existing spring framework application uses Thymeleaf which is responsible for webpage generation (Server Side Rendering). Using Vue in this scenario is not an issue because it can be embedded directly into HTML pages like normal JavaScript. We utilized the server-side conditional statements to redact or hide pieces of JavaScript code inside the generated webpages. The code snippet below shows how Thymeleaf checks if the user has the authority before rendering the specific part of the webpage.

Thymeleaf conditionals inside an inline Javascript

/*[# sec:authorize="hasAnyAuthority('SYSTEM_ADMINISTRATOR', 'OPERATION_ADMINISTRATOR')"]*/

The code from line 4 to 21 will only be rendered if the user has the required authorization.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
methods: {
    voidOrder()
    {
        /*[# sec:authorize="hasAnyAuthority('SYSTEM_ADMINISTRATOR', 'OPERATION_ADMINISTRATOR')"]*/
        try {
            let payload = {};
            let config = {};
            config.headers = {};
            config.headers[document.querySelector('meta[name="_csrf_header"]').content] = document.querySelector('meta[name="_csrf"]').content;
            axios.post('/admin/void-order', payload, config)
                .then(function (response) {
                    //response handler here
                }.bind(this))
                .catch(function (error) {
                    //error handler here
                }.bind(this));

        } catch (err) {
            console.log(err);
        }
        /*[/]*/
    }
}

Learning curve

My team and I had experience coding in JavaScript, Java and Go. It is only practical to choose a framework where we can reuse existing skill set. We decided to go with Vue because of its neat coding structure and fantastic attribute, event and input binding.

Class and Style binding

<div class="panel" :class="{ active: isActive, 'text-danger': hasError }"></div>

Event binding

<button @click="viewOrder">View Order</button>

Input binding

<input v-model="order.OrderNumber" placeholder="Order Number" />

Reuse or retrofit existing jQuery components

The project used a number of jQuery and JavaScript components from Telerik and other vendors. Reusing and wrapping them into Vue would save us a lot of time. The existing components were already production-tested and the customer didn’t want to replace them. Telerik had Vue components, but we decided to keep using the existing jQuery-based components

Wrapping a jQuery component into Vue 2 (prj-datetime-picker.js)

Vue.component('prj-datetime-picker', {
    template: `<input :id="id" :value="value" :format="format" />`,
    props: {
        id: {type: String},
        value: {type: String},
        format: {type: String},
        callback: {type: Function, default: null},
        disabled: {
            default: false,
        }
    },
    data() {
        return {}
    },
    watch:
        {
            disabled(v) {
                let x = $("#" + this.id).data("kendoDateTimePicker");
                x.enable(!v);
            },
            value: {
                handler(v) {
                    let x = $("#" + this.id).data("kendoDateTimePicker");
                    if (x !== undefined) {
                        x.value(v);
                    }
                },
                immediate: true,
                deep: true,
            },
        },
    methods: {
        onChange() {
            this.$emit('change', $("#" + this.id).data("kendoDateTimePicker").value());
            if (this.callback !== null) {
                this.callback(this.id, $("#" + this.id).data("kendoDateTimePicker").value());
            }
        }
    },
    mounted() {
        $(this.$el).kendoDateTimePicker({
            format: this.format,
            value: this.value,
            change: this.onChange,
        });

        let x = $("#" + this.id).data("kendoDateTimePicker");
        x.enable(!this.disabled);
    },
})

We can then use this wrapped jQuery component like a regular Vue component


<prj-datetime-picker id="orderDateTime"
                     format="dd/MM/yyyy HH:mm" style="width: 100%"
                     :value="order.startDateTime"
                     @change="onOrderDateTimeChange">
</prj-datetime-picker>

Conclusion

We completed the project and the customer were very happy with the final software product. It resulted in more modular code that was easy to maintain, change and extend. Furthermore, we also learned to choose a technology based on existing team skill set and project timeline, use frameworks or libraries to deliver business solutions, reuse or retrofit production-tested components, use both modular monolith and microservices to our advantage and not to blindly follow the latest technology trends and marketing buzz.