Background

I would like to share my experience using Vue 2 without a build step in my project and factors using this approach. I started experimenting with Vue.js (Vue 2) back in 2020. One of the nice feature of Vue is the ability to use the framework directly into HTML page without a build tool (compilation). This particular feature is the deciding factor why I selected 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 a 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 are 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 actual tasks migrating the code and improving the business logic. Angular needed a build step to transpile TypeScript to Javascript while Vue and React can be used without a build step but the latter requires 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 codes into its own dedicate 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 a normal Javascript code. 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 codes 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 have 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 uses a number of jQuery and Javascript components from Telerik and other vendors. Reusing and wrapping them into Vue will save us a lot of time. The existing components were already production tested and the customer don’t want to replace them. Telerik have Vue components, but decided to keep using the existing jQuery based components

Wrapping 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 are very happy with the final software product. It resulted in a more modular code and super 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 solution, reuse or retrofit production tested components, use both modular monolith and microservices to your advantage and to not blindly follow latest technology trends and marketing buzz.