VueJS getting "undefined" data from ipcRenderer (ElectronJS) - vue.js

When trying to get a message from ipcMain to ipcRenderer (without node integration and with contextIsolation), it's received but as undefined. Not only that, but if I were to reload the VueComponent (regardless of what change I make to it), the number of responses gets doubled.
For example, the first time I start my application, I get 1x undefined at a time every time I click the button. If I reload the component, I start getting 2x undefined every time I click the button. I reload again and get 4x undefined every time I click the button... and it keeps doubling. If I restart the application, it goes back to 1x.
SETUP
ElectronJS + VueJS + VuetifyJS has been set up as described here.
preload.js as per the official documentation.
import { contextBridge, ipcRenderer } from 'electron'
window.ipcRenderer = ipcRenderer
// Expose protected methods that allow the renderer process to use
// the ipcRenderer without exposing the entire object
contextBridge.exposeInMainWorld('ipcRenderer', {
send: (channel, data) => {
// whitelist channels
let validChannels = ['toMain']
if (validChannels.includes(channel)) {
ipcRenderer.send(channel, data)
}
},
receive: (channel, func) => {
let validChannels = ['fromMain']
if (validChannels.includes(channel)) {
// Deliberately strip event as it includes `sender`
ipcRenderer.on(channel, (event, ...args) => func(...args))
}
}
})
background.js (main process) as per the official documentation for the preload.js file. The omitted code via ... is the default project code generated upon creation.
...
const path = require('path')
const { ipcMain } = require('electron')
async function createWindow() {
// Create the browser window.
const win = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
// Use pluginOptions.nodeIntegration, leave this alone
// See nklayman.github.io/vue-cli-plugin-electron-builder/guide/security.html#node-integration for more info
nodeIntegration: process.env.ELECTRON_NODE_INTEGRATION,
contextIsolation: true,
preload: path.join(__dirname, 'preload.js'),
},
icon: 'src/assets/icon.png',
})
ipcMain.on('toMain', (event, data) => {
console.log(data)
event.sender.send('fromMain', 'Hello IPC Renderer')
// The two lines below return 'undefined' as well in the 'ipcRenderer'
//win.webContents.send('fromMain', "Hello IPC Renderer")
//event.reply('fromMain', 'Hello IPC Renderer')
})
...
}
...
vue.config.js file:
module.exports = {
...
pluginOptions: {
electronBuilder: {
preload: 'src/preload.js',
}
}
}
main.js (renderer process) contains only the default project code generated upon creation.
VueComponent.vue
<template>
<div id="vue-component">
<v-btn #click="sendMessageToIPCMain()">
</div>
</template>
<script>
export default {
name: "VueComponent",
components: {
//
},
data: () => ({
myData: null,
}),
methods: {
// This works. I get 'Hello IPC Main' in the CMD console.
sendMessageToIPCMain() {
var message = "Hello IPC Main"
window.ipcRenderer.send("toMain", message);
}
},
mounted() {
window.ipcRenderer.receive('fromMain', (event, data) => {
// this.myData = data // 'myData' is not defined error
this.$refs.myData = data;
console.log('myData variable: ' + this.$refs.myData) // undefined
console.log(data) // undefined
})
},
}
</script>
The VueComponent.vue's mounted() has been set up as described here, though If I try to send the data to a variable using this.myData = data, I get an error saying that myData has not been defined - using this.$refs.myData works, though it's still undefined.
P.S. myData has not been defined error =/= undefined. The former is a proper error in red letters while the latter is as seen in the image above.

For solving the first problem (doubling of function calls) you have to remove window.ipcRenderer = ipcRenderer. In contextIsolation mode the approach is to use contextBridge.exposeInMainWorld() only. Using both implementation definitely causes issues.
For the second problem, the callback to receive in ipcRenderer is called with only ...args from main (no event passed to func). see:
ipcRenderer.on(channel, (event, ...args) => func(...args)) <-- func() is called with only args
The only thing you should change is your function in mounted, to accept only data:
window.ipcRenderer.receive('fromMain', (data) => {
console.log(data) // should log you data
})

Related

How to use highlight.js in a VueJS app with mixed content

I'm currently using highlight.js to hightlight the code in the HTML content being received from my backend. An example of something I might receive from the backend is:
<h3> Check this example of Javascript </h3>
<pre>
<code class="language-javascript">let x = 0;
function hello() {}
</code>
</pre>
As you can see it is a mixed content of HTML and code examples wrapped in pre -> code tags.
I have a component to render WYSIWYG content returned from the backend. In this component, I use highlight.js to highlight the code blocks.
import { defineComponent, h, nextTick, onMounted, ref, watch } from 'vue';
// No need to use a third-party component to highlight code
// since the `#tiptap/extension-code-block-lowlight` library has highlight as a dependency
import highlight from 'highlight.js'
import { QNoSsr } from 'quasar';
export const WYSIWYG = defineComponent({
name: 'WYSIWYG',
props: {
content: { type: String, required: true },
},
setup(props) {
const root = ref<HTMLElement>(null);
const hightlightCodes = async () => {
if (process.env.CLIENT) {
await nextTick();
root.value?.querySelectorAll('pre code').forEach((el: HTMLElement) => {
highlight.highlightElement(el as HTMLElement);
});
}
}
onMounted(hightlightCodes);
watch(() => props.content, hightlightCodes);
return function render() {
return h(QNoSsr, {
placeholder: 'Loading...',
}, () => h('div', {
class: 'WYSIWYG',
ref: root,
innerHTML: props.content
}));
};
},
});
Whenever I visit the page by clicking on a link the page works just fine, but when I hard refresh the page I get the following error:
`line` must be greater than 0 (lines start at line 1)
Currently, I'm not sure precisely why this happens, and tried a couple of different approaches
Aproach 1: try to build the whole content and then replace
const computedHtml = computed(() => {
if (import.meta.env.SSR) return '';
console.log(props.content);
const { value } = highlight.highlightAuto(props.content);
console.log(value);
return '';
})
With this approach, I get the same error as before
`line` must be greater than 0 (lines start at line 1)
I have checked out this error in https://github.com/withastro/astro/issues/3447 and https://github.com/vitejs/vite/issues/11037 but it looks like that this error is more related to Vite than my application - please, correct me if I'm wrong here.
Is there a way for me to highlight the code in the backend that is being returned from the backend in Vue?

Using XState in Nuxt 3 with asynchronous functions

I am using XState as a state manager for a website I build in Nuxt 3.
Upon loading some states I am using some asynchronous functions outside of the state manager. This looks something like this:
import { createMachine, assign } from "xstate"
// async function
async function fetchData() {
const result = await otherThings()
return result
}
export const myMachine = createMachine({
id : 'machine',
initial: 'loading',
states: {
loading: {
invoke: {
src: async () =>
{
const result = await fetchData()
return new Promise((resolve, reject) => {
if(account != undefined){
resolve('account connected')
}else {
reject('no account connected')
}
})
},
onDone: [ target: 'otherState' ],
onError: [ target: 'loading' ]
}
}
// more stuff ...
}
})
I want to use this state machine over multiple components in Nuxt 3. So I declared it in the index page and then passed the state to the other components to work with it. Like this:
<template>
<OtherStuff :state="state" :send="send"/>
</template>
<script>
import { myMachine } from './states'
import { useMachine } from "#xstate/vue"
export default {
setup(){
const { state, send } = useMachine(myMachine)
return {state, send}
}
}
</script>
And this worked fine in the beginning. But now that I have added asynchronous functions I ran into the following problem. The states in the different components get out of sync. While they are progressing as intended in the index page (going from 'loading' to 'otherState') they just get stuck in 'loading' in the other component. And not in a loop, they simply do not progress.
How can I make sure that the states are synced in all my components?

Vue 2 / Nuxt 2 Emit From Axios With Dialog Confirm

i am using Vue 2 / nuxt to emit from a axios post* call which itself is called from a Buefy dialog confirm. The emit from this component will close the window / panel and then re-load the users.
If I call the axios request from the button, this works without any issues, but once being called from the dialog, it just don't work?
*most likely this will be updated to a delete request, just not gotten to that let
See code below:
removeUser() {
this.$buefy.dialog.confirm({
message: 'Continue on this task?',
onConfirm: () => {
this.removeUserFunc()
}
})
},
removeUserFunc() {
// console.log(that)
const that = this
// Build URL
const EndPoint = '/remove_user/' + this.id
this.$axios.post(EndPoint).then((res) => {
// User Remove Message
UserRemoved(this.$swal)
that.$parent.$emit('completed')
// console.log(this.$emit('complete'))
// // Emit 'completed' Message
console.log(that.$emit('completed'))
console.log(that)
}).catch((res) => {
console.log(res)
// Check For Errors
GeneralError(this.$swal)
})
}
I was thinking it was losing access to the correct this, so i was trying to pass that back in, but not sure that is the case?
I have also tried with await, while that sort of works? I think is firing the emit too fast, as it re-loads the users but it still includes the user that as just been deleted?
removeUser() {
this.$buefy.dialog.confirm({
message: 'Continue on this task?',
onConfirm: async() => {
this.removeUserFunc()
await this.$emit('completed')
}
})
},
The this keyword refers to the object the function belongs to, or the window object if the function belongs to no object.
Try to use .bind and use a ES5 function
removeUser() {
this.$buefy.dialog.confirm({
message: 'Continue on this task?',
onConfirm: function() {
this.removeUserFunc()
}.bind(this)
})
},

Receive WebSockets data from vuex and Vue-native-websocket plugin

I am currently using the Quasar V1 framework which includes Vue and Vuex.
Today I was looking at this plugin:
https://www.npmjs.com/package/vue-native-websocket/v/2.0.6
I am unsure on how to setup this plugin and make it work and would require a little bit of help to make sure I am doing this right as it will be the first time I use WebSockets with Vue.
I have first installed vue-native-websocket via npm and created a boot file called src\boot\websocket.js
via this command:
npm install vue-native-websocket --save
websocket.js
import VueNativeSock from 'vue-native-websocket';
export default async ({ Vue }) => {
Vue.use(VueNativeSock, 'wss://echo.websocket.org', {
reconnection: true,
reconnectionAttempts: 5,
reconnectionDelay: 3000
});
};
In Quasar v1, I have then created a module called "websockets" in:
src\store\websockets
This module has:
actions.js
getters.js
index.js
mutations.js
state.js
I need to use the websocket with format: 'json' enabled
My question is:
Let's say I have a page where I would like my websocket connection to be created and receive the live data, shall I do this?
Code for the module:
websockets/mutations.js:
export function SOCKET_ONOPEN (state, event) {
let vm = this;
vm.prototype.$socket = event.currentTarget;
state.socket.isConnected = true;
}
export function SOCKET_ONCLOSE (state, event) {
state.socket.isConnected = false;
}
export function SOCKET_ONERROR (state, event) {
console.error(state, event);
}
// default handler called for all methods
export function SOCKET_ONMESSAGE (state, message) {
state.socket.message = message;
}
// mutations for reconnect methods
export function SOCKET_RECONNECT (state, count) {
console.info(state, count);
}
export function SOCKET_RECONNECT_ERROR (state) {
state.socket.reconnectError = true;
}
Code for the module:
websockets/state.js
export default {
socket: {
isConnected: false,
message: '',
reconnectError: false
}
};
But the issue now is in my vue page.
Let's say I would like to show only the data from the websocket that has a specific event, how do I call this from the vue page itself please? I am very confused on this part of the plugin.
What is very important for me to understand if how to separate the receive and send data.
ie: I may want to receive the list of many users
or I may want to receive a list of all the news
or I may add a new user to the database.
I keep hearing about channels and events and subscriptions......
From what I understand, you have to first subscribe to a channel(ie: wss://mywebsite.com/news), then listen for events, in this case I believe the events is simply the data flow from this channel).
If I am correct with the above, how to subscribe to a channel and listen for events with this plugin please, any idea?
If you had a very quick example, it would be great, thank you.
I have developed a chat application using Vue-native-websocket plugin. Here i am showing how you can register the pulgin in the vuex store and how to call it from your vue component.
Step 1: Define these methods in your index.js file
const connectWS = () => {
vm.$connect()
}
const disconnectWS = () => {
vm.$disconnect()
}
const sendMessageWS = (data) => {
if (!Vue.prototype.$socket) {
return
}
Vue.prototype.$socket.send(JSON.stringify(data))
}
Step 2: Write the socket state and mutations
SOCKET_ONOPEN (state, event) {
if (!state.socket.isConnected) {
Vue.prototype.$socket = event.currentTarget
state.socket.isConnected = true
let phone = state.config.selectedChatTicket.phone
sendMessageWS({type: WSMessageTypes.HANDSHAKE, data: {id: window.ACCOUNT_INFO.accId, phone: phone, agentId: USER_NAME}})
}
},
SOCKET_ONCLOSE (state, event) {
console.log('SOCKET_ONCLOSE', state, event)
state.socket.isConnected = false
Vue.prototype.$socket = null
},
// NOTE: Here you are getting the message from the socket connection
SOCKET_ONMESSAGE (state, message) {
state.data.chatCollection = updateChatCollection(state.data.chatCollection,message)
},
STEP 3 : Write Action, you can call it from your vue component
NOTE:: socket actions to connect and disconnect
WSConnect ({commit, state}) {
connectWS()
},
WSDisconnect ({commit, state}) {
disconnectWS()
},
STEP 4: Register the plugin in the end as it requires the store object
Vue.use(VueNativeSock, `${window.location.protocol === 'https:' ? 'wss' : 'ws'}://www.example.com/socketserver`,
{ store: store, format: 'json', connectManually: true })
STEP 5: call your action from your vue component
buttonClick (rowData) {
const tickCount = this.ticketClickCounter
if (tickCount === 0) {
this.$store.dispatch('WSConnect')
} else {
this.$store.dispatch('WSDisconnect')
setTimeout(() => {
this.$store.dispatch('WSConnect')
}, 1000)
}
this.ticketClickCounter = tickCount + 1
},
Now you are connected to the socket
STEP 6: write a action method in your vuex file
sendChatMessageAction ({commit, state}, data) {
// NOTE: Here, you are sending the message through the socket connection
sendMessageWS({
type: WSMessageTypes.MESSAGE,
data: {
param1: abc,
param2: xyz,
param3: 123,
param4: $$$
}
})
},
STEP 7: you can define a input text box and on-enter evenlisterner you can call the action method
onEnter (event) {
if (event.target.value !== '') {
let newValue = {
param1: Date.now(),
param2: xyz,
param3: 123,
}
this.$store.dispatch('sendChatMessageAction', newValue) // Action
}
},

Mounted hook running before Created data api is finished loading

I am trying to load a function when images from a data api are finished loading. However, it looks like the function is run before the ApiService is finished and thus the TiffParser.replaceIMG() function is not working properly
Here's my setup:
data: function() {
return {
images: null,
imageLink: apiService.imgSrc,
loading: true,
errored: false
};
},
created: function() {
// fetch the data when the view is created and the data is
// already being observed
apiService
.getImages(this.$route.params.id)
.catch(error => {
console.log(error);
this.errored = true;
})
.then(response => {
this.loading = false;
this.images = response.data;
});
},
//vue js provides us `mounted()`. This means `onload` in javascript
mounted: function() {
TiffParser.replaceIMG();
}
Is mounted the correct lifecycle hook for this task?
You can create a watcher for your images.
created() {
const unwatch = this.$watch('images', function(newValue = [], oldValue = []) {
// any code here will execulte once the value of `images` changes
TiffParser.replaceIMG();
unwatch(); // remove the watcher
// Note that you cannot use ES6 arrow functions here, since arrow functions
// are bound to the parent context, and the `this` keyword
// would then not be bound correctly to the Vue instance.
});
// fetch images
}
Is mounted the correct lifecycle hook for this task?
Yes, if you need to access or modify the DOM of your component immediately before or after the initial render.
However, images would be empty when it's first mounted so using a watcher instead of the mounted hook seems more appropriate for this use case.