Vuejs and vue-socketio: how to pass socket instance to components - javascript

I am trying to use Vuejs together with the Vue-Socket.io plugin and have a question regarding the correct way of passing the socket between components.
Ideally I want to use only one socket in my whole app, so I want to instantiate the socket in the root instance and then pass it down to the components that need it as a 'prop'. Is that the correct way of doing it?
If so, what am I doing wrong? The error I receive is TypeError: this.socket.emit is not a function so I am probably not passing the socket object correctly.
The component using the socket has the following script
<script>
export default {
name: 'top',
props: ['socket'],
data () {
return {
Title: 'My Title'
}
},
methods: {
button_click: function (val) {
// This should emit something to the socketio server
console.log('clicking a button')
this.socket.emit('vuejs_inc', val)
}
}
}
</script>
My initialization in the root component looks like this:
import Vue from 'vue'
import VueSocketio from 'vue-socket.io'
import App from './App'
import router from './router'
Vue.use(VueSocketio, 'http://127.0.0.1:5000')
Vue.config.productionTip = false
/* eslint-disable no-new */
new Vue({
el: '#app',
router,
template: '<App socket="this.$socket"/>',
components: {App},
sockets: {
connect: function () {
console.log('Vuejs socket connected.')
},
from_server: function (val) {
console.log('Data from server received: ' + val)
}
}
})
And App then passes the socket via
<top socket="this.socket"></top>
NB: I know that I could also either put the socket instantiation into the component that needs it (in this case: top), or I could access the socket object from the root component via this.$root.$socket, but I don't want to do either, because
As stated above, I might want to use the socket in other components
I don't just want to assume a socket object is there in the root instance
In essence, I want to do it right from an architectural standpoint.

There is no need to pass anything. The Vue-Socket.io plugin makes the socket available on every component (and Vue) via this.$socket.
I think your code would work except for the fact that you do not appear to be binding the socket.
<top socket="this.socket"></top>
Should be
<top :socket="socket"></top>
The first line above will just set socket to the string "this.socket". The second will set the property to the result of the expression, this.socket.
You would also need to change the template for the Vue:
<App :socket="$socket"/>

Related

Vue component inside legacy php/jquery project

I've got this big legacy web app based on Codeigniter and jQuery.
The plan is to phase out jQuery and start using Vuejs instead. We are replacing certain parts of the website step by step.
I have just installed Nuxtjs and got the file structure the way i like it inside the legacy project.
Now to the question. How can i access a Vue component from one of my legacy javascript files?
App.vue
<template>
<div id="app">
<HelloWorld msg="Welcome to Your Vue.js + TypeScript App" />
</div>
</template>
<script lang="ts">
import Vue from "vue";
import HelloWorld from "./components/HelloWorld.vue";
export default Vue.extend({
name: "App",
components: {
HelloWorld
}
});
</script>
main.ts
import Vue from "vue";
import App from "./App.vue";
import store from "./store";
Vue.config.productionTip = false;
new Vue({
store,
render: h => h(App)
}).$mount("#app");
I want to display my App.vue inside an ordinary php/html view.
I am doing something similar right now, the project was originally written using Thymeleaf and jQuery and now we are switching to Vue.
You can communicate between vue components and non-vue components in a few ways, none of them are "pretty".
Communication
Classic JavaScript events
Pretty straightforward
// legacy code
document.dispatchEvent(new CustomEvent('legacy-vue.something-happened', { detail: payload }))
// vue component
created () {
document.addEventListener('legacy-vue.something-happened', this.onSomethingHappened)
},
destroyed () { // don't forget to remove the listener!
document.removeEventListener('legacy-vue.something-happened', this.onSomethingHappened)
}
Exposing EventHub
Similar to the previous one, but you are using vue events instead. This is what i would recommend, because it's the Vue way of handling events and your goal is to vuetify your app.
// initialization
const hub = new Vue()
Vue.prototype.$eventHub = hub
window.$vueEventHub = hub
// legacy code
$vueEventHub.$emit('something-happened', payload)
// vue component
created () {
this.$eventHub.$on('something-happened', this.onSomethingHappened)
},
destroyed () {
this.$eventHub.$off('something-happened', this.onSomethingHappened)
}
Exposing whole components
The most flexible way, but it's hard to see what's going on where. Event based approaches the lesser evil in my opinion (it's easy to track events).
// vue component
created () {
window.vueTableComponent = this
}
// legacy component
vueTableComponent.fetchNextPage()
vueTableComponent.registerOnPageFetchedCallback(callback);
Summary
No matter which approach you pick, i recommend doing something like this:
Let's say that you have TableComponent. TableComponent has few props like apiUrl, emits row-clicked event, etc. It would be best to design the component without thinking about your legacy code at all, and then create it's legacy wrapper because at one point it will be used both with vue-only screens and with mixed-screens (with both legacy components and vue components). An example:
LegacyTableComponentWrapper.vue
<template>
<table-component
:api-path="apiPath"
#row-clicked="onRowClicked"
ref="table-component"
/>
</template>
export default {
data: () => ({
apiPath: null
}),
methods: {
onRowClicked (row) {
this.$eventHub.$emit('table-row-clicked', row) // notify legacy code
},
onApiPathDefined (payload) {
this.apiPath = payload
}
},
mounted () {
// legacy code might require the TableComponent to act differently
// and if you don't want the TableComponent to know whenever it's legacy or not
// you can always override one or more of it's methods.
this.$refs['table-component'] = this.overriddenImplementationOfXYZ
},
created () {
this.$eventHub.$on('define-api-path', this.onApiPathDefined)
},
destroyed () {
this.$eventHub.$off('define-api-path', this.onApiPathDefined)
}
}
It's sure more work at first, but will save you a headache later, when you will be working on your first view which is fully in vue and all that legacy communication stuff is in the way.

Vue Mixin properties are blank/empty/not reactive

I hope this question is not a duplicate. If it is so, please point me to the right directions.
I have a Vue application which is compiled with Webpack#NPM. I use mixin to propagate a property (roles) across all components. I update it with an ajax call from app instantiation. Problem is roles only updates for the <Root> component, not for all others.
////////////////////////
// app.js
////////////////////////
// import
window.axios = require('axios')
import Vue from 'vue'
import VueRouter from 'vue-router'
import routes from './routes.js'
// mixin
Vue.mixin({
data: function () {
return {
// property in question
roles: [],
}
},
methods: {
getRoles: function() { //////////// this method updates property.
// get
axios.get('/api/vcr/admin/roles')
// process
.then(response=>{
this.roles = response.data.data;
})
// error?
.catch(error=>{
this.toast(error.response.data.message);
})
},
},
});
// router
const router = new VueRouter({
mode: 'history',
routes: routes,
});
// app
const app = new Vue({
el: '#app',
components: { App: require('./views/App').default },
router,
base: '/saas/vcr/admin',
created: function() { ////////////// I update it here
this.getRoles();
}
});
////////////////////////
// Foo.vue
////////////////////////
<script>
export default {
mounted: function() {
console.log(this.roles) ////// returns an empty array
}
}
</script>
Do you know how to make roles reactive?
The global mixin you have created does not call the function that populates the roles property, it relies on the inheriting instance to do so. In your app "root" instance, you're doing that in the created life-cycle hook which calls getRoles on the mixin, but in the component Foo you are not calling it, so it will have its default empty value. The roles property is not shared, each component will get its own copy of it and will need to be populated.
You could change the mixin to do this for you, by adding the life-cycle created hook as you have done in the root instance. Here's an example of that. Note implementing that in the mix-in does not prevent or override later life cycle hooks from being run on the instances it is merged into. But, it will in your case make an API call for every component instance that is created, which probably isn't desirable.
If you want to only populate it once then share it between all components, it might make more sense to use Vuex and have a global state where roles is populated centrally and shared between all components in a reactive way.

Add all vue components to window array

I currently have a strange Vue setup due to our websites all using an old system.
What we have had to do is create an instance of Vue for each component (usually not many). What I want to do for all components is to pass their name and reference to the element into an array, just for reference when debugging issues on live issues.
app.js
import Vue from "vue";
import Axios from 'axios';
import inViewportDirective from 'vue-in-viewport-directive';
window.components = [];
Vue.component( 'video-frame', () => import('./components/VideoFrame.vue' /* webpackChunkName: "video-frame" */) );
Vue.prototype.$event = new Vue();
Vue.prototype.$http = Axios;
Array.prototype.forEach.call(document.querySelectorAll(".app"), (el, index) => new Vue({el}));
Now i'm adding the following code to each component, is there not a way I can do this once within my app.js and have all the components automatically do the following:
mounted() {
window.components.push({
tag: this.$vnode.tag,
elm: this.$vnode.elm
});
},
You can use a global mixin like this:
Vue.mixin({
mounted: function() {
window.components.push({
tag: this.$vnode.tag,
elm: this.$vnode.elm
});
}
});
That will ensure that code will run on the mounted hook on every single one of your Vue instances.
Reference: https://v2.vuejs.org/v2/guide/mixins.html

Vue.js Dynamic use string

I am trying to use vue-native-websocket and the wss is dynamic because it needs an access token.
So I have this:
import VueNativeSock from 'vue-native-websocket';
Vue.use(VueNativeSock, this.monitor);
export default {
created() {
this.$options.sockets.onmessage = this.handleToggle;
},
data() {
return {
type: false
};
},
methods: {
handleToggle(data) {
const object = JSON.parse(data.data);
console.log(object);
this.status = object.data;
}
},
props: ['items', 'monitor']
};
As you can see, the prop monitor is passed but when it comes time to do Vue.use()... I get undefined. How do I pass a dynamic variable to a Vue use?
Vue.use is for registering plugins globally.
I didn't use vue-native-websocket before. But
import VueNativeSock from 'vue-native-websocket';
Vue.use(VueNativeSock, ....);
should be done in the place where Vue is initiated (where new Vue({...}) is, it's in main.js in a typical vue-cli generated project.) . Not inside component.
Vue.use should also be executed before new Vue({...}) in main.js.
Therefore, I don't think you would be able to make wss dynamic, unless this plugin has a specified method for you do to that.

How to define property with a component?

I currently have three steps in a form that I want to show sequentially, so I created three components - one for each step of the process.
My app.js file:
import LocationList from './components/LocationList.vue';
import ChooseTime from './components/ChooseTime.vue';
import ChooseMethod from './components/ChooseMethod.vue';
Vue.component('location-list', LocationList);
Vue.component('choose-time', ChooseTime);
Vue.component('choose-method', ChooseMethod);
let store = {
isVisible: {
steps: {
one: true,
two: false,
three: false,
}
}
};
new Vue({
el: '#app-order',
data: store,
router
});
Now, when my one and only route is called,
import VueRouter from 'vue-router';
let routes = [
{
path: '/order',
component: require('./views/Order.vue')
}
];
export default new VueRouter({
routes
});
all these components are being loaded properly. The issue is that when I try to v-show them one at a time:
Order.vue:
<template>
// ...
<location-list v-show="isVisible.steps.one"></location-list>
<choose-time v-show="isVisible.steps.two"></choose-time>
<choose-method v-show="isVisible.steps.three"></choose-method>
// ...
</template>
<script>
</script>
<style>
</style>
The error message I receive is:
[Vue warn]: Property or method "isVisible" is not defined on the instance but referenced during render. Make sure to declare reactive data properties in the data option.
But when I check within Vue's browser extension, isVisible is defined within the root element?
As you can see it is in the root-element, but not inside the Order view though.
Thanks for any help!
In Vue, child components do not have direct access to data defined in their parents. You have to pass the data down.
I think you would probably save yourself a little trouble if you just defined isVisible in Order.vue. However, if you want to leave it where it is, you need to pass it into the component.
One easy way to do that is to define isVisble as a property of Order.vue and then pass it through your router-view.
<router-view :is-visible="isVisible"></router-view>
There are other ways of passing props to routes that are defined in the router documentation.
The reason I say you would save your self some trouble defining isVisible in Order.vue is because whenever you want to change the values of your steps, you will need to do it at the root as you currently have it defined.

Categories