Creating an Apfell - Part 5
Published:
We have three more components to cover before our initial release of this section of Apfell. Last time, we covered websockets and how to get data asynchronously to the JavaScript runngin in a user’s browser. But all we did was log it to the console. That’s not very exciting, and it doesn’t actually make anything useful for us. So, we need a way to automatically update the document object model (DOM) of our webpage when we get data through our websocket. Welcome, Vue.js
Vue
Vue.js provides a way to do “data-reactive components” in web interfaces. That’s a fancy way of saying exactly what we want - updating our webpage without having to referesh to include the data we’re getting from our websockets.
This works with two components: data integration into our Jinja2, HTML templates, and data manipulation with JavaScript. Let’s look at a very simple example from Vue’s website to see what’s going on:
The Jinja2, HTML side:
<div id="demo">
<p>{{message}}</p>
<input v-model="message">
</div>
and the JavaScript side:
var vueDemoObj = new Vue({
el: '#demo',
data: {
message: 'Hello Vue.js!'
}
})
Ok, so, what’s going on here and what does this actually do? You can mess with the code here while we walk through it a bit. The HTML code has a div tag with an id of “demo”. This is how the JavaScript code will know which element to reference. From here, there’s a new paragraph created with the contents of something called message
, which will be explained in a bit. Below that paragraph, we see an input field. The default text of this input field is set to the value of the message
variable, but there’s a new property: v-model
. This directive creates a two-way binding on editable elements where data is synced on every input event.
Let’s look at the Vue code to tie it all together. We create a new Vue object that contains all the information for this HTML code we want to control First, el
refers to the DOM element we’re referring to for the rest of the Vue object. In our case, this is “demo” - you can see this as id="#demo"
in our HTML code. Next, we call out the data
variables and potential default values. This information is expressed in standard JSON format for easy manipulation and description. In our case, we have one variable, message
that has a default value of the String “Hello Vue.js”.
As you can see by messing with the JSFiddle code, when you manipulate the data in the input box, the data in the paragraph above it changes instantly to reflect the changes. This is because we said in our HTML code that we will print the value of the message
variable in our paragraph, and the value of the message
variable is tied to the input field. You can mess around with these fields to see how it all works together.
There are lots of really cool things you can do with Vue, but we don’t have the time to go over all of them here. So, we’ll just cover a few things that we’ll use immediately.
methods and delimiters
You might not have noticed, but we just used {{ }}
for Vue. But, didn’t we use that a few posts ago for Jinja2 directives? Yup! That’s gonna be an issue for us because Jinja2 will try to process that data before it gets to the point where Vue can access it. Luckily, Vue provides a handy way around this: delimiters
. For example, we can change those brackets to be whatever we want. In our case, I’ll change them to [[ ]]
instead.
var obj = new Vue({
el: '#someId',
data:{
variable: 'value'
},
methods: {
interact_button: function(callback){
alert("clicked button " + callback['pid']);
}
},
delimiters: ['[[',']]']
});
We know that Jinja2 doesn’t use double square brackets, so that’s ok for us to use with Vue. These values can be set to whatever we want though.
Additionally, Vue doesn’t only control static elements, but can control function behavior as well (such as on click). In the above JavaScript example, we defined the function interact_button
that takes a parameter, callback
, and creates an alert. How do we tie this to our code though?
<!--- normally --->
<button type="button" class="btn btn-primary" onClick="interact_button()">Interact</button>
<!--- Vue style - normal --->
<button type="button" class="btn btn-primary" v-on:"click: interact_button(callback)">Interact</button>
<!--- Vue style - shortcut --->
<button type="button" class="btn btn-primary" @click="interact_button(callback)">Interact</button>
This is a little abnormal for HTML syntax. Normally, the v-on
directive binds event listeners to DOM events, calling either event handlers (they will have () after them), or inline expressions. A shortcut for this (Vue has a few different kinds of shortcuts) is to use the @ symbol as the 3rd line shows.
v-for
We can use the v-for directive to do looping through arrays. For example, consider us having an array of all the callbacks we currently have in a variable called callbacks
. We can do something like:
<table id="table">
<!-- Repeat this for each callback -->
<tr v-for="callback in callbacks" :key="callback.id">
<td> [[ callback.host ]] </td>
<td> [[ callback.ip ]] </td>
<!-- End of the repeating -->
</table>
var newObj = new Vue({
el: '#table',
data: {
callbacks : [
{
host : 'hostname1',
ip : '127.0.0.1',
id : 1
},
{
host : 'hostname2',
ip : '192.168.0.1',
id : 2
}
]
},
delimiters: ['[[',']]']
});
This will create a table where each row is created dynamically based on the existence of the row in the callbacks array. For each row, we’ll display the hostname and IP in different columns. There are lots of things you can start doing in this regard by connecting JavaScript to your DOM.
websockets, Vue, DOM, oh my
Now let’s take a quick example to see how to connect this new information to our bigger project.
callbacks = [];
var callback_table = new Vue({
el: '#callback_table',
data: {
callbacks
},
methods: {
interact_button: function(callback){
alert("clicked button " + callback['pid']);
}
},
delimiters: ['[[',']]']
});
function startwebsocket(){
var ws = new WebSocket('ws://127.0.0.1/ws/callbacks');
ws.onmessage = function(event){
cb = JSON.parse(event.data);
callbacks.push(cb);
};
ws.onclose = function(){
console.log("socket closed");
}
ws.onerror = function(){
console.log("websocket error");
}
ws.onopen = function(event){
console.debug("opened");
}
};
startwebsocket();
Now, every time we get new JSON data through our websocket, we parse it and add it to our callbacks array. So we need to tie this callbacks array to our DOM in some way to get it to update automatically.
<div style="resize: vertical; overflow: auto" class="panel panel-primary" id="callback_table">
<div class="panel-heading">Current callbacks</div>
<div class="well well-sm pre-scrollable">
<table class="table table-striped table-hover">
<tr>
<td></td>
<td><b>Host</b></td>
<td><b>IP</b></td>
<td><b>User</b></td>
<td><b>PID</b></td>
<td><b>Initial Checkin</b></td>
<td><b>Last Checkin</b></td>
<td><b>Spawning Operator</b></td>
<td><b>Description</b></td>
</tr>
<!-- Repeat this for each callback -->
<tr v-for="callback in callbacks" :key="callback.id">
<td>
<!-- Split button -->
<div class="btn-group">
<button type="button" class="btn btn-primary" @click="interact_button(callback)">Interact</button>
<button type="button" class="btn btn-info dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<span class="caret"></span>
<span class="sr-only">Toggle Dropdown</span>
</button>
<ul class="dropdown-menu">
<li><a href="#">Action</a></li>
<li><a href="#">Another action</a></li>
<li><a href="#">Something else here</a></li>
<li role="separator" class="divider"></li>
<li><a href="#">Separated link</a></li>
</ul>
</div>
</td>
<td><p>[[ callback.host ]]</p></td>
<td><p>[[ callback.ip ]]</p></td>
<td><p>[[ callback.user ]]</p></td>
<td><p>[[ callback.pid ]]</p></td>
<td><p>[[ callback.init_callback ]]</p></td>
<td><p>[[ callback.real_time ]]</p></td>
<td><p>[[ callback.operator ]]</p></td>
<td><p>[[ callback.description ]]</p></td>
</tr>
<!-- End of the repeating -->
</table>
</div>
</div>
As you can see, we are now connecting our callback information, in a v-for
loop to add new rows to our table. We added in everything we talked here like the functions with @click
, new delimiters, and loops.
I’ve left some exercises for you to include new actions under the dropdown-menu
and add more information. You can include this HTML code in a new route inside a pair of {% block body%}{% endlbock %}
so that you can browse to it via your browser.
Now that you can get the real-time updates actually seen in your browser thanks to Vue, we have one more main topic to cover next time (forms and user authentication) before we release all of the code for this so far.