Making a Real-Time Electron App with Vue, Vuetify, and Butterfly Server .NET

In a recent post, I walked through the steps to create a real-time web app to manage a todo list using Butterfly Server .NET.

In another post, I walked through the steps to create a Cordova app that can be used to access our todo list in real-time.

In this post, we’ll again reuse the same server but create an Electron app that can access our todo list in real-time.

Just Let Me Try It

Prefer to skip the step-by-step instructions?

Run this in a terminal or command prompt…

git clone https://github.com/firesharkstudios/butterfly-server-dotnet
cd butterfly-server-dotnet\Butterfly.Example.Todo
dotnet run -vm

Run this in a second terminal or command prompt…

# This assumes you have Electron already installed
cd butterfly-server-dotnet\Butterfly.Example.Todo\electron
npm install
npm run dev

Or follow the instructions below to build your Electron app from scratch.

Building the Electron App

This assumes you have Electron already installed.

Let’s again use npm to create our client…

npm install -g vue-cli
# Just accept the defaults...
vue init vuetifyjs/electron my-todo-electron-client
cd my-todo-electron-client
npm install
npm install butterfly-client reqwest

The above commands…

Next, let’s edit src/main/index.js to turn off web security in the browser to allow our API requests and WebSocket requests to access our local Butterfly Server .NET

mainWindow = new BrowserWindow({
webPreferences: { webSecurity: false },
height: 563,
useContentSize: true,
width: 1000
})

Next, edit src/main.js to add a couple of imports…

import { ArrayDataEventHandler, WebSocketChannelClient } from 'butterfly-client'
import reqwest from 'reqwest'

Next, edit src/renderer/main.js to replace the existing new Vue() call with…

new Vue({
el: '#app',
components: { App },
router,
template: '<App/>',
data() {
return {
channelClient: null,
channelClientState: null,
}
},
methods: {
callApi(url, rawData) {
let fullUrl = `http://localhost:8000${url}`;
return reqwest({
url: fullUrl,
method: 'POST',
data: JSON.stringify(rawData),
});
},
subscribe(options) {
let self = this;
self.channelClient.subscribe({
channel: options.key,
vars: options.vars,
handler: new ArrayDataEventHandler({
arrayMapping: options.arrayMapping,
onInitialEnd: options.onInitialEnd,
onChannelMessage: options.onChannelMessage
}),
});
},
unsubscribe(key) {
let self = this;
self.channelClient.unsubscribe(key);
},
},
beforeMount() {
let self = this;
    let url = `ws://localhost:8000/ws`;
self.channelClient = new WebSocketChannelClient({
url,
onStateChange(value) {
self.channelClientState = value;
}
});
self.channelClient.connect();
},
})

The above code…

  • Creates a WebSocketChannelClient instance that maintains a WebSocket connection to our Butterfly Server .NET
  • Defines a callApi() method that our client can use to invoke API calls
  • Defines subscribe() and unsubscribe() methods that our client can use to subscribe/unsubscribe to specific channels on our Butterfly Server .NET

Next, edit src/renderer/App.vue to contain (identical to App.vue from the prior post)…

<template>
<v-app>
<v-content>
<v-toolbar>
<v-toolbar-title>My Todo Example</v-toolbar-title>
<v-spacer />
</v-toolbar>
<router-view v-if="$root.channelClientState=='Connected'"/>
<div class="px-5 py-5 text-xs-center" v-else>
<v-progress-circular indeterminate color="primary"/>
<span class="pl-2 title">
{{ $root.channelClientState }}...
</span>
</div>
</v-content>
</v-app>
</template>

The above template will cause the main content of our page to show a loading indicator until our WebSocketChannelClient has successfully connected to our Butterfly Server .NET.

Next, edit src/renderer/components/WelcomeView.vue (identical to HelloWorld.vue from the prior post) to contain…

<template>
<v-container fluid>
<Todos />
</v-container>
</template>
<script>
import Todos from '@/components/Todos'
  export default {
components: {
Todos,
}
}
</script>

Next, create a new src/renderer/components/Todos.vue (identical to Todos.vue from the prior post) which contains…

<template>
<div>
<div class="px-3 py-3 text-xs-center" v-if="items.length==0">
No todos yet
</div>
<v-list v-else>
<Todo v-for="item in items" :key="item.id" :item="item" @remove="remove" />
</v-list>
<div class="px-3 py-3 text-xs-center">
<v-btn color="primary" @click="add">Add Todo</v-btn>
</div>
</div>
</template>
<script>
import Todo from '@/components/Todo'
export default {
components: {
Todo,
},
data () {
return {
items: [],
}
},
methods: {
add() {
this.$root.callApi('/api/todo/insert', {
name: 'A new todo item',
});
},
remove(id) {
this.$root.callApi('/api/todo/delete', id);
},
},
mounted() {
let self = this;
self.$root.subscribe({
arrayMapping: {
todo: self.items,
},
key: 'todos',
});
}
}
</script>

The above Todos.vue is responsible for…

Next, create a new src/renderer/components/Todo.vue (identical to Todo.vue from the prior post) which contains…

<template>
<v-list-tile>
<v-list-tile-content>
<v-list-tile-title>{{ item.name }}</v-list-tile-title>
</v-list-tile-content>
<v-list-tile-action>
<v-btn icon @click="$emit('remove', item.id)">
<v-icon>delete</v-icon>
</v-btn>
</v-list-tile-action>
</v-list-tile>
</template>
<script>
export default {
props: {
item: null
}
}
</script>

The above Todo.vue is responsible for rendering a single todo.

Finally, disable eslint by commenting out this section in .electron-vue/webpack.renderer.config.js

/*
{
test: /\.(js)$/,
enforce: 'pre',
exclude: /node_modules/,
use: {
loader: 'eslint-loader',
options: {
formatter: require('eslint-friendly-formatter')
}
}
},
*/

Trying It

The end result will look something like this…

First, run the server within Visual Studio (either create the server per the prior post or clone from GitHub and run Butterfly.Example.Todo.Server)

Next, run the Electron app by executing (in the my-todo-electron-client directory) …

npm run dev

Next Steps

The Butterfly Server .NET supports building much more sophisticated real-time web apps (can subscribe to channels with multiple datasets, can join multiple tables in each dataset, etc). See GitHub for more details.