Call event bus with this.$root.$emit - javascript

I have a simple Event bus that changes styles on a page and it works. The event bus is called with the name of the event bus and $emit and $on:
EventBus.$on
and
EventBus.$emit('call-modal', { type: 'success' });
How can I make so that instead of calling it with $on and $emit I can call it with this.$root.$emit so that I can use it in every other component? I tried but currently it doesn't work, why?
Here is my App.vue:
<template >
<div id="app">
<bankAccount>
</bankAccount>
<div :class="['modal', `modal--type--${modalType}`]" v-show="showModal">
<slot name="title">e</slot>
<slot name="description"></slot>
</div>
</div>
</template>
<script>
import bankAccount from './components/bankAccount.vue'
import Vue from 'vue'
export const EventBus = new Vue()
export default {
name: 'app',
components: {
bankAccount,
},
data() {
return {
showModal: false,
modalType: 'default',
}
},
created() {
EventBus.$on('call-modal', obj => {
this.showModal = true
this.modalType = obj.type
})
},
}
</script>
<style>
.modal {
height: 100px;
width: 300px;
border: solid gray 2px;
}
.modal--type--success {
border-color: green;
}
.modal--type--danger {
border-color: red;
width: 100%;
}
.modal--type--warning {
border-color: yellow;
width: 100%;
}
</style>
And my component:
<template>
<div>
<button class="pleeease-click-me" #click="callModal()">Click me</button>
</div>
</template>
<script>
import { EventBus } from '../App.vue';
export default {
name: 'bankAccount',
data() {
return {
showModal: false
}
},
methods: {
callModal() {
this.showModal = !this.showModal
EventBus.$emit('call-modal', { type: 'success' });
}
}
}
</script>
<style scoped>
.modal {
height: 100px;
width: 300px;
}
</style>

You can create a separate eventbus.js file and include it to all of your components. This way, they'll be using the same eventbus instance. TL;DR: it's been explained here:
https://alligator.io/vuejs/global-event-bus/
Long one:
Create an eventbus.js file with this content:
import Vue from 'vue';
export const EventBus = new Vue();
Then include it:
import { EventBus } from './event-bus.js';
Then use it:
EventBus.$on("event", function(data){
// do stuff with data
});
with
EventBus.$emit("event", data);
and don't forget to remove the event on destruction:
beforeDestroy {
EventBus.$off("event");
}

Simply add that to the instance prototype :
// main.js
//import vue from 'vue'
Vue.prototype.$eventHub = new Vue();
// new Vue({
// ...
// })
now you can use it on any component with :
this.$eventHub.$emit('call-modal');

To achieve desired result you have to slightly change your code.
In your App.vue change these lines of code:
created() {
this.$root.$on('call-modal', obj => {
this.showModal = true
this.modalType = obj.type
})
},
And then in your component:
callModal() {
this.showModal = !this.showModal
this.$root.$emit('call-modal', { type: 'success' })
}
Notice that approach suggested by #Dadboz, is preferable, than you desirable.

Related

Vue: Teleport inheritAttrs false not working

I'm trying to create a BaseOverlay component that basically teleports its content to a certain area of my application. It works just fine except there's an issue when using it with v-show... I think because my component's root is a Teleport that v-show won't work because Teleport is a template.
I figured I could then use inheritAttrs: false and v-bind="$attrs" on the inner content... this throws a warning from Vue saying Runtime directive used on component with non-element root node. The directives will not function as intended. It results in v-show not working on MyComponent, but v-if does work.
Any clues as to what I'm doing wrong?
Example
App.vue
<script setup>
import MyComponent from "./MyComponent.vue";
import {ref} from "vue";
const showOverlay = ref(false);
function onClickButton() {
showOverlay.value = !showOverlay.value;
}
</script>
<template>
<button #click="onClickButton">
Toggle Showing
</button>
<div id="overlays" />
<div>
Hello World
</div>
<MyComponent v-show="showOverlay" text="Doesn't work" />
<MyComponent v-if="showOverlay" text="Works" />
</template>
BaseOverlay.vue
<template>
<Teleport to="#overlays">
<div
class="overlay-container"
v-bind="$attrs"
>
<slot />
</div>
</Teleport>
</template>
<script>
export default {
name: "BaseOverlay",
inheritAttrs: false,
};
</script>
MyComponent.vue
<template>
<BaseOverlay>
{{text}}
</BaseOverlay>
</template>
<script>
import BaseOverlay from "./BaseOverlay.vue";
export default {
name: "MyComponent",
components: {
BaseOverlay
},
props: {
text: {
type: String,
default: ""
}
}
}
</script>
I would consider moving the modal/overlay dependency out of the component and into the app composition to make it more reusable.
Note the isMounted check check - this is to add a delay if the outlet containers have not yet been defined. You may need to add additional handling if your outlets are not pressent on mount e.g. <div id="outlet" v-if="false">
const {
createApp,
defineComponent,
ref,
onMounted
} = Vue
//
// Modal
//
const MyModal = defineComponent({
name: 'MyModal',
props: {
to: {
type: String,
required: true
},
show: Boolean
},
setup(){
const isMounted = ref(false);
onMounted(()=> isMounted.value = true )
return { isMounted }
},
template: `
<teleport :to="to" v-if="isMounted">
<div class="overlay-container" v-show="show">
<slot />
</div>
</teleport>
`
})
//
// Component
//
const MyComp = defineComponent({
name: 'MyComp',
template: `<div>Component</div>`
})
//
// App
//
const MyApp = defineComponent({
name: 'MyApp',
components: {
MyModal,
MyComp
},
setup() {
const modalShow = ref(false);
const modalOutlet = ref('#outletOne');
const toggleModalShow = () => {
modalShow.value = !modalShow.value
}
const toggleModalOutlet = () => {
modalOutlet.value = modalOutlet.value == '#outletOne'
? '#outletTwo'
: '#outletOne'
}
return {
toggleModalShow,
modalShow,
toggleModalOutlet,
modalOutlet,
}
},
template: `
<div>
<button #click="toggleModalShow">{{ modalShow ? 'Hide' : 'Show' }} Modal</button>
<button #click="toggleModalOutlet">Toggle Modal Outlet {{ modalOutlet }} </button>
</div>
<MyModal :to="modalOutlet" :show="modalShow">
<MyComp />
</MyModal>
<div id="outletOne">
<h2>Outlet One</h2>
<!-- outlet one -->
</div>
<div id="outletTwo">
<h2>Outlet Two</h2>
<!-- outlet two -->
</div>
`
})
//
// Assemble
//
const app = createApp(MyApp)
app.mount('body')
/* just some styling */
#outletOne { color: tomato; }
#outletTwo { color: olive; }
h2 { margin: 0; }
[id*=outlet]{ display: inline-flex; flex-direction: column; padding: 1rem; }
button { margin: 1rem; }
<script src="https://unpkg.com/vue#3.1.4/dist/vue.global.js"></script>
Wanted to follow-up on this. I started running into a lot of other issues with Teleport, like the inability to use it as a root element in a component (like a Dialog component because I want to teleport all dialogs to a certain area of the application), and some other strange issues with KeepAlive.
I ended up rolling my own WebComponent and using that instead. I have an OverlayManager WebComponent that is used within the BaseOverlay component, and every time a BaseOverlay is mounted, it adds itself to the OverlayManager.
Example
OverlayManager.js
export class OverlayManager extends HTMLElement {
constructor() {
super();
this.classList.add("absolute", "top-100", "left-0")
document.body.appendChild(this);
}
add(element) {
this.appendChild(element);
}
remove(element) {
this.removeChild(element);
}
}
customElements.define("overlay-manager", OverlayManager);
BaseOverlay.vue
<template>
<div class="overlay-container" ref="rootEl">
<slot />
</div>
</template>
<script>
import {ref, onMounted, onBeforeUnmount, inject} from "vue";
export default {
name: "BaseOverlay",
setup() {
const rootEl = ref(null);
const OverlayManager = inject("OverlayManager");
onMounted(() => {
OverlayManager.add(rootEl.value);
});
onBeforeUnmount(() => {
OverlayManager.remove(rootEl.value);
});
return {
rootEl
}
}
};
</script>
App.vue
<script setup>
import {OverlayManager} from "./OverlayManager.js";
import MyComponent from "./MyComponent.vue";
import {ref, provide} from "vue";
const manager = new OverlayManager();
provide("OverlayManager", manager);
const showOverlay = ref(false);
function onClickButton() {
showOverlay.value = !showOverlay.value;
}
</script>
<template>
<button #click="onClickButton">
Toggle Showing
</button>
<div>
Hello World
</div>
<MyComponent v-show="showOverlay" text="Now Works" />
<MyComponent v-if="showOverlay" text="Works" />
</template>
<style>
.absolute {
position: absolute;
}
.top-100 {
top: 100px;
}
.left-0 {
left: 0;
}
</style>
This behaves exactly how I need it, and I don't have to deal with the quirks that Teleport introduces, and it allows me to have a singleton that is in charge of all of my overlays. The other benefit is that I have access to the parent of where BaseOverlay is initially added in the HTML (not where it's moved). Honestly not sure if this is a good practice, but I'm chuffed at how cute this is and how well Vue integrates with it.

Trying to hide model when clicked outside has different effects

I have a vue modal component which I make visible/invisible depending on a boolean variable, I also want to hide the modal when the user clicks outside the modal, I do this by attaching a click listener and checking each click to see if its outside or inside however there must be something wrong with my approach becuase the modal automatically closes right after I click the button to make it visible.
This is my component:
<template>
<transition name="cart-tab">
<div class="ADMINsearch_maincontainer" v-show="filtersVisible" id="admin-search">
</div>
</transition>
</template>
<!--SCRIPTS-->
<script>
import { mapState, mapGetters, mapActions, mapMutations } from 'vuex';
export default {
name: 'LAYOUTsearch',
computed:
{
...mapState('AdminPanel',['filtersVisible', 'panelSchema', 'theme', 'searchSchema']),
},
data(){
return {
specifiedElement:null
}
},
mounted()
{
console.log(this.$options.name+' component successfully mounted');
this.addListener();
},
methods:
{
addListener()
{
this.specifiedElement = document.getElementById('admin-search')
const self = this;
document.addEventListener('click', function(event)
{
var isClickInside = self.specifiedElement.contains(event.target);
if (!isClickInside)
{
//outside
self.setFiltersVisible(false)
}
else
{
//inside
}
});
},
}
};
</script>
<!--STYLES-->
<style scoped>
.container_style{width:100% !important;}
.ADMINsearch_maincontainer{width:33%; height:100vh; z-index:9999999999999999; background-color:white; box-shadow:-3px -3px 6px 6px rgba(0,0,0,0.3); position:fixed; top:0px; right:0px; display:flex; flex-direction:column;}
}
</style>
The issue could be that you are getting the value of the variable filtersVisible trough mapState, then you are trying to update its value with a method in the component instance.
Let's suppose a store with this configuration:
const state = {
filtersVisible: false
}
const mutations = {
setFiltersVisibleValue(state, visible) {
state.filtersVisible = visible
}
}
export default {
state,
mutations,
}
In the component you can set and get the state of the filtersVisible variable this way:
<template>
<transition name="cart-tab">
<div
class="ADMINsearch_maincontainer"
v-show="filtersVisible"
id="admin-search"
></div>
</transition>
</template>
<!--SCRIPTS-->
<script>
import { mapState, mapMutations } from "vuex";
export default {
name: "LAYOUTsearch",
computed: {
...mapState(["filtersVisible"])
},
data() {
return {
specifiedElement: null
};
},
mounted() {
console.log(this.$options.name + " component successfully mounted");
this.addListener();
},
methods: {
...mapMutations(["setFiltersVisibleValue"]),
addListener() {
this.specifiedElement = document.getElementById("admin-search");
document.addEventListener("click", function(event) {
var isClickInside = self.specifiedElement.contains(event.target);
if (!isClickInside) {
//outside - call the mutation to update properly the value of the filtersVisible variable in the store
this.setFiltersVisibleValue(false);
} else {
//inside
}
});
}
}
};
</script>
<!--STYLES-->
<style scoped>
.container_style{width:100% !important;}
.ADMINsearch_maincontainer{width:33%; height:100vh; z-index:9999999999999999; background-color:white; box-shadow:-3px -3px 6px 6px rgba(0,0,0,0.3); position:fixed; top:0px; right:0px; display:flex; flex-direction:column;}
}
</style>
The example is using only the variable filtersVisible for the explanation purpose. I'm assuming a Vuex store that is not using modules.

Video.js with Vue.js (MPEG-DASH) ERROR: (CODE:4 MEDIA_ERR_SRC_NOT_SUPPORTED)

I have problem with Video.js with i use it as component in vue.js.
I recieve a .mpd link from a Server and i want to show the video from the link,
i did like the example in documentation Video.js and Vue integration.
always the first time i call the VideoPlayer showed an Error:
VIDEOJS: ERROR: (CODE:4 MEDIA_ERR_SRC_NOT_SUPPORTED) No compatible source was found for this media.
When i go out to the previous page and then to the VideoPlayer again it works. ( also do not works when i refresh the page )
P.s: i use vuex to get all data from Server.
Here is my Code for Stream.vue:
<template>
<div class="container">
<h1 class="text-center">MediaPlayer for: {{ mediaName }}</h1>
<video-player :options="videoOptions" />
</div>
</template>
<script>
import { mapState, mapActions } from "vuex";
import VideoPlayer from "#/components/VideoPlayer.vue";
export default {
name: "Stream",
props: ["stream_id", "source"],
components: {
VideoPlayer
},
created() {
this.fetchStream(this.stream_id);
},
computed: {
...mapState("stream", ["stream", "mediaName"]),
videoOptions() {
return {
autoplay: false,
controls: true,
sources: [
{
src: this.stream.stream_link,
type: "application/dash+xml"
}
],
poster:"http://placehold.it/380?text=DMAX Video 2"
};
}
},
methods: {
...mapActions("stream", ["fetchStream"])
}
};
</script>
<style scoped></style>
and Here is VideoPlayer.vue:
<template>
<div>
<video ref="videoPlayer" class="video-js"></video>
</div>
</template>
<script>
import videojs from "video.js";
export default {
name: "VideoPlayer",
props: {
options: {
type: Object,
default() {
return {};
}
}
},
data() {
return {
player: null
};
},
mounted() {
this.player = videojs(
this.$refs.videoPlayer,
this.options,
function onPlayerReady() {
console.log("onPlayerReady", this);
}
);
},
beforeDestroy() {
if (this.player) {
this.player.dispose();
}
}
};
</script>
I have found how to solve this problem.
The problem was that the VideoPlayer will be rendered first then it tried to get the link from the Store.
How to solve this:
you have to try to get the link before the render of VideoPlayer, I made another component VueVideoPlayer. In this component, I do all requests and wait till I get all information, and then call the VideoPlayer.vue component and pass the options as props.
VueVideoPlayer.vue
<template>
<div>
<div v-if="loading">
<div class="text-center">
<div
class="spinner-border m-5 spinner-border-lg"
style="width: 3rem; height: 3rem; border-top-color: rgb(136, 255, 24);
border-left-color:
rgb(136, 255, 24);
border-right-color:
rgb(136, 255, 24);
border-bottom-color:
rgb(97, 97, 97); "
role="status"
>
<span class="sr-only">Loading...</span>
</div>
</div>
</div>
<div v-else>
<video-player :options="videoOptions" />
</div>
</div>
</template>
<script>
import VideoPlayer from "#/components/VideoPlayer.vue";
import StreamsServices from "../services/StreamsServices";
import NProgress from "nprogress";
import CookieService from "../services/CookieSerice";
export default {
name: "VueVideoPlayer",
components: {
VideoPlayer
},
data() {
return {
player: null,
stream_link: "",
loading: false
};
},
created() {
this.loading = true;
NProgress.start();
StreamsServices.getStream(this.stream_id, this.settings)
.then(response => {
this.stream_link = response.data.stream_link;
})
.finally(() => {
NProgress.done();
this.loading = false;
this.keepAlive();
this.interval = setInterval(this.keepAlive, 20000);
});
},
props: ["stream_id", "settings"],
computed: {
videoOptions() {
return {
autoplay: false,
controls: true,
sources: [
{
src: this.stream_link,
type: "application/dash+xml"
}
],
poster: "http://placehold.it/380?text=DMAX Video 2"
};
}
},
methods: {
keepAlive() {
CookieService.getToken().then(token => {
StreamsServices.postKeepAlive({
token: token,
audiopreset: this.settings.videoPresetId,
videopreset: this.settings.audioPresetId,
transcodedVideoUri: this.stream_link
}).then(() => {});
});
}
}
};
</script>
PS: here I did not use the store/stream.js, i did direct the request using the StreamsServices.

How to import a script into .vue file?

I am trying to import a player into my vue.js file. Normally I would use a script file outside of the template, but that does not work.
In an html file what I would do is the following:
<div id="player">
<div id="vplayer"></div>
</div>
<script type="text/javascript" src="https://cdn.jsdelivr.net/npm/clappr#latest/dist/clappr.min.js"></script>
<script>
var urlsrc = "http://www.streambox.fr/playlists/x36xhzz/x36xhzz.m3u8";
var player = new Clappr.Player({source: urlsrc, parentId: "#vplayer", height: 240, width: 320});
</script>
In vue.js I am trying to do the same thing with the following return code, but it does not work:
<template>
<div id="player">
<div id="vplayer"></div>
</div>
</template>
<script>
export default {
name: 'player',
data () {
return {
script: 'https://cdn.jsdelivr.net/npm/clappr#latest/dist/clappr.min.js',
url: 'http://www.streambox.fr/playlists/x36xhzz/x36xhzz.m3u8',
player: new Clappr.Player({source: this.url, parentId: "#vplayer", height: 240, width: 320})
}
}
}
</script>
I get an error saying that the player is undefined. How can I get normal scripts to run inside of vue.js?
One possible solution is adding external script in created() hooks and use script onload method
<template>
.... your HTML
</template>
<script>
export default {
data: () => ({
url: 'http://www.streambox.fr/playlists/x36xhzz/x36xhzz.m3u8',
player: null
}),
created() {
let clapprScript = document.createElement('script')
clapprScript.setAttribute('src', 'https://cdn.jsdelivr.net/npm/clappr#latest/dist/clappr.min.js')
clapprScript.onload = this.initPlayer;
document.head.appendChild(clapprScript)
},
methods: {
initPlayer() {
this.player = new Clappr.Player({
source: this.url,
parentId: "#vplayer",
height: 240,
width: 320
})
console.log('Player is loaded')
}
}
}
</script>
You should use Mixins. Make your own mixin and then add the methods from imported script inside methods section of mixin.
var mixin = {
methods: {
foo: function () {
console.log('foo')
},
conflicting: function () {
console.log('from mixin')
}
}
}
var vm = new Vue({
mixins: [mixin],
methods: {
bar: function () {
console.log('bar')
},
conflicting: function () {
console.log('from self')
}
}
})
You should be able to do this, after installing Clappr with npm:
<script>
import Clappr from 'clappr';
export default {
created() {
let player = Clappr.Player({...});
}
}
</script>
This way you can initialize your clappr instance in the Vue component just as you would in vanilla html, js scenario.

Vue JS alert box

I'm new to VueJS.
I had written an alert box using jquery. Now I'm trying to provide that in VueJS.
Here is what I have done:
1- Created a component named NovinAlert.vue including:
<template>
<div id="novin-alert-container">
<div v-for="message in messages" class="alert novin-alert" :class="message.class">
×
{{ message.body }}
</div>
</div>
</template>
<script>
export default {
data: function () {
return {
messages: [],
}
},
methods: {
novinAlert: function (message) {
let app = this;
message.type = typeof message.type !== 'undefined' ? message.type : 'info';
message.class = 'alert' + message.type;
let index = app.messages.push(message) - 1;
if (typeof message.duration !== 'undefined') {
window.setTimeout(function () {
app.messages.splice(index, 1);
}, message.duration * 1000);
}
}
}
}
</script>
2- My example component is Dashboard.vue
<template>
...
</template>
<script>
export default {
mounted() {
novinAlert('test');
}
}
</script>
3- My main layout is:
<body>
<div id="app">
<novin-alert></novin-alert>
<router-view></router-view>
</div>
</body>
4- And this is my app.js:
require('./bootstrap');
window.Vue = require('vue');
import VueRouter from 'vue-router';
window.Vue.use(VueRouter);
import Dashboard from './components/Dashboard.vue';
const routes = [
{path: '/', component: Dashboard, name: 'dashboard'},
];
import NovinAlert from './components/utils/NovinAlert.vue';
Vue.component('novin-alert', NovinAlert);
const router = new VueRouter({ routes: routes });
const app = new Vue({
router,
el: '#app'
});
When I run dashboard page, it doesn't know novinAlert function.
What I have done wrong? And what is best practice for what I'm trying to do?
Any help is appreciated.
i've designed a SIMPLE popup right now with Vuejs 2.* and it works well. In this example, popup is included as a Local component. This would be one of the best practices to follow. Let me know if this helps.
App.vue
<template>
<div>
<div>Main page</div>
<button #click="openpopup">Open popup</button>
<popup :popupData="popupData" ></popup>
</div>
</template>
<script>
import Popup from'./Popup.vue';
export default {
components:{
"popup" : Popup
},
data() {
return {
popupData : {
"header" : "My popup",
"body" : "hello world",
"footer" : "divine inc.",
"display" : "none"
}
}
},
methods : {
openpopup(){
this.popupData.display = "block";
}
},
mounted(){
this.popupData.display = "block";
}
}
</script>
Popup.vue
<template>
<div id="popup" :style="{display : popupData.display}">
<div class="inner">
<div class="header">
<div>{{popupData.header}}</div>
<div #click="closeFunction">Close</div>
</div>
<div class="body">{{popupData.body}}</div>
<div class="footer">{{popupData.footer}}</div>
</div>
</div>
</template>
<script>
export default {
props : ["popupData"],
methods : {
closeFunction(){
this.popupData.display = "none";
}
}
}
</script>
<style>
html,body{
padding: 0;
margin:0;
}
#popup{
position: absolute;
width: 100%;
height :100%;
top: 0;
}
#popup .inner{
background-color: green;
position: inherit;
top: 10%;
left: 39%;
width: 300px;
}
#popup .inner div{
text-align: left;
}
#popup .inner .header{
display: flex;
justify-content: space-between;
}
</style>
You can't access one component's method from another. you need to move the novinAlert function to the Dashboard component and pass messages as a property to NovinAlert component:
NovinAlert.vue
<template>
<div id="novin-alert-container">
<div v-for="message in messages" class="alert novin-alert" :class="message.class">
×
{{ message.body }}
</div>
</div>
</template>
<script>
export default {
props: ['messages']
}
</script>
Dashboard.vue
<template>
<!-- pass the messages prop to the component -->
<novin-alert :messages="messages"></novin-alert>
</template>
<script>
export default {
mounted() {
this.novinAlert('test');
},
data() {
return {
messages: []
}
},
methods: {
novinAlert: function (message) {
let app = this;
message.type = typeof message.type !== 'undefined' ? message.type : 'info';
message.class = 'alert' + message.type;
let index = app.messages.push(message) - 1;
if (typeof message.duration !== 'undefined') {
window.setTimeout(function () {
app.messages.splice(index, 1);
}, message.duration * 1000);
}
}
}
}
</script>
if you want the novin alert in the app level, you'll need to pass the function to the app component
This is what I finally did:
I created an instance of Vue in my app.js:
window.eventBus = new Vue();
I use this event but for multiple purposes. Whenever I need I trigger(emit) an event and the listener does required action.
For alert box, I emit the event like this:
window.eventBus.$emit('alert', {
text: app.trans('general.error_in_reading_data'),
type: 'danger',
duration: 5,
});
Then my alert component reacts like this:
mounted() {
window.eventBus.$on('alert', (message) => {
this.novinAlert(message);
});
},

Categories