Add Vue.js event on window - javascript

Vue.js allow apply event on element:
<div id="app">
<button #click="play()">Play</button>
</div>
But how to apply event on window object? it is not in DOM.
for example:
<div id="app">
<div #mousedown="startDrag()" #mousemove="move($event)">Drag me</div>
</div>
in this example, how to listen mousemove event on window ?

You should just do it manually during the creation and destruction of the component
...
created: function() {
window.addEventListener('mousemove',this.move);
},
destroyed: function() {
window.removeEventListener('mousemove', this.move);
}
...

Jeff's answer is perfect and should be the accepted answer. Helped me out a lot!
Although, I have something to add that caused me some headache. When defining the move method it's important to use the function() constructor and not use ES6 arrow function => if you want access to this on the child component. this doesn't get passed through to arrow functions from where it's called but rather referrers to its surroundings where it is defined. This is called lexical scope.
Here is my implementation with the called method (here called keyDown instead of move) included:
export default {
name: 'app',
components: {
SudokuBoard
},
methods: {
keyDown: function () {
const activeElement = document.getElementsByClassName('active')[0]
if (activeElement && !isNaN(event.key) && event.key > 0) {
activeElement.innerHTML = event.key
}
}
},
created: function () {
window.addEventListener('keydown', this.keyDown)
},
destroyed: function () {
window.removeEventListener('keydown', this.keyDown)
}
}
To be extra clear, The method below doesn't have access to this and can't for example reach the data or props object on your child component.
methods: {
keyDown: () => {
//no 'this' passed. Can't access child's context
}

You can also use the vue-global-events library.
<GlobalEvents #mousemove="move"/>
It also supports event modifiers like #keydown.up.ctrl.prevent="handler".

This is for someone landed here searching solution for nuxt.js:
I was creating sticky header for one of my projects & faced issue to make window.addEventListener work.
First of all, not sure why but window.addEventListener doesn't work with created or beforeCreate hooks, hence I am using mounted.
<template lang="pug">
header(:class="{ 'fixed-header': scrolled }")
nav menu here
</template>
<script>
export default {
name: 'AppHeader',
data() {
return { scrolled: false };
},
mounted() {
// Note: do not add parentheses () for this.handleScroll
window.addEventListener('scroll', this.handleScroll);
},
methods: {
handleScroll() {
this.scrolled = window.scrollY > 50;
},
},
};
</script>

I found that using window.addEventListener no longer worked as I expected with Vue 3. It appears that the function is wrapped by Vue now. Here's a workaround:
...
created()
{
document.addEventListener.call(window, "mousemove", event =>
{
...
});
}
...
The important part is document.addEventListener.call(window, what we're doing is to take the unwrapped addEventListener from document and then call it on the window Object.

Related

best way to incorporate vanilla event listeners on multiple components

I have 2 event listeners defined in the mounted lifecycle of a component, that allow a user to close out of a v-dialog by either double clicking outside or pressing escape. Writing these vanilla event listeners was the only way I knew to achieve the functionality that I wanted. Something like this:
<template>
<v-container>
<v-dialog :value="value" max-width="55vw" height="70vw" persistent>
...some stuff...
</v-dialog>
</v-container>
</template>
<script>
export default {
name: "a name",
props: {
value: { required: true },
},
mounted() {
let that = this;
document.addEventListener("keyup", function (evt) {
if (evt.key === "Escape") {
that.closeDialog();
}
});
document.addEventListener("dblclick", function (e) {
if (e.target == document.getElementsByClassName("v-overlay__scrim")[0]) {
that.closeDialog();
}
});
},
unmounted() {
window.removeEventListener("keyup");
window.removeEventListener("dblclick");
},
methods: {
closeDialog() {
this.$emit("input");
},
},
};
</script>
I would like to extend this functionality of closing out of v-dialogs to several other components, but adding these event listeners to multiple components' mounted block seems redundant. Is there a better way to achieve this?
In my opinion, you shouldn't use native events for this case. Also adding an event listener over the document on an specific Vue component, could lead to confusions, since you're always listening to that event everywhere (in your case, "esc" key being pushed). Imagine if you have a large number of components and when you push "esc", some code is being executed. If you're not the author of this piece of code, it's "hard" to tell from where it comes.
For this case I would recommend: using a Mixin that could contain the lifecycle hooks (vue 2) or using a composable for importing the function (vue 2 with vue/composition-api or vue 3) and manually registering it with the proper lifecycle hook function.
BTW, on your code, on the remove event listener part, you should pass the function reference. So it would be better to extract the method to a proper vue method of that component.
mounted() {
let that = this;
document.addEventListener("keyup", this.close);
document.addEventListener("dblclick", this.close2);
},
unmounted() {
window.removeEventListener("keyup", this.close);
window.removeEventListener("dblclick", this.close2);
},
methods: {
close(evt) {
if (evt.key === "Escape") {
this.$emit("input");
}
},
close2(e) {
if (e.target == document.getElementsByClassName("v-overlay__scrim")[0]) {
this.$emit("input");
}
},
},

Cant get Vue to focus on input

I'm trying to use this.$refs.cInput.focus() (cInput is a ref) and it's not working. I'd be able to hit g and the input should pop up and the cursor should focus in it, ready to input some data. It's showing but the focus part is not working. I get no errors in the console.
Vue.component('coordform', {
template: `<form id="popup-box" #submit.prevent="process" v-show="visible"><input type="text" ref="cInput" v-model="coords" placeholder =""></input></form>`,
data() {
{
return { coords: '', visible: false }
}
},
created() {
window.addEventListener('keydown', this.toggle)
},
mounted() {
},
updated() {
},
destroyed() {
window.removeEventListener('keydown', this.toggle)
},
methods: {
toggle(e) {
if (e.key == 'g') {
this.visible = !this.visible;
this.$refs.cInput.focus() //<--------not working
}
},
process() {
...
}
}
});
You can use the nextTick() callback:
When you set vm.someData = 'new value', the component will not
re-render immediately. It will update in the next “tick”, when the
queue is flushed. [...]
In order to wait until Vue.js has finished updating the DOM after a data
change, you can use Vue.nextTick(callback) immediately after the data
is changed. The callback will be called after the DOM has been
updated.
(source)
Use it in your toggle function like:
methods: {
toggle(e) {
if (e.key == 'g') {
this.visible = !this.visible;
this.$nextTick(() => this.$refs.cInput.focus())
}
}
}
In my case nextTick does not worked well.
I just used setTimeout like example below:
doSearch () {
this.$nextTick(() => {
if (this.$refs['search-input']) {
setTimeout(() => {
this.$refs['search-input'].blur()
}, 300)
}
})
},
I think that for your case code should be like below:
toggle(e) {
if (e.key == 'g') {
this.visible = !this.visible;
setTimeout(() => { this.$refs.cInput.focus() }, 300)
}
}
Not sure if this is only applicable on Vue3 but if you want to show focus on an input box from inside a component, it's best for you to setup a transition. In the transition, there is an event called #after-enter. So after the animation transition on enter is completed, the #after-enter is executed. The #after-enter will be the event that will always get executed after your component shows up.
I hope this helps everyone who are having issues with the:
***this.$nextTick(() => this.$refs.searchinput.focus());***
It may not be working the way you wanted it to because chances are the initial focus is still on the parent and the input element has not been displayed yet or if you placed it under mounted, it only gets executed once and no matter how you show/hide the component (via v-if or v-show), the nextTick line was already executed on mount() and doesn't get triggered on show of the component again.
Try this solution below:
<template>
<transition name="bounce" #after-enter="afterEnter" >
<div>
<input type="text" ref="searchinput" />
</div>
</transition>
</template>
<script>
methods: {
afterEnter() {
setTimeout(() => {
this.$refs.searchinput.focus();
}, 200);
},
}
</script>

Vuejs single file component (.vue) model update inside the <script> tag

I'm new to vuejs and I'm trying to build a simple single file component for testing purpose.
This component simply displays a bool and a button that change the bool value.
It also listen for a "customEvent" that also changes the bool value
<template>
{{ mybool }}
<button v-on:click="test">test</button>
</template>
<script>
ipcRenderer.on('customEvent', () => {
console.log('event received');
this.mybool = !this.mybool;
});
export default {
data() {
return {
mybool: true,
};
},
methods: {
test: () => {
console.log(mybool);
mybool = !mybool;
},
},
};
</script>
The button works fine. when I click on it the value changes.
but when I receive my event, the 'event received' is displayed in the console but my bool doesn't change.
Is there a way to access the components data from my code?
Thanks and regards,
Eric
You can move ipcRenderer.on(...) into vuejs's lifecycle hooks like created.
See: https://v2.vuejs.org/v2/guide/instance.html#Instance-Lifecycle-Hooks
You are setting up the event listener outside of the component's options which you export by using
export default{ //... options }
Set up the event listener inside the vue options so the vue instance has control over it, in your case modifying dara property
As choasia suggested move the event listener to `created() life cycle hook:
<template>
{{ mybool }}
<button v-on:click="test">test</button>
</template>
<script>
export default {
data() {
return {
mybool: true,
};
},
methods: {
test: () => {
console.log(mybool);
mybool = !mybool;
},
},
created(){
ipcRenderer.on('customEvent', () => {
console.log('event received');
this.mybool = !this.mybool;
});
}
};
</script>
Now you component will starting listening for that particular even when the component is created

Event from parent to child component

I have event that is generated in parent component and child has to react to it. I know that this is not recommended approach in vuejs2 and i have to do a $root emit which is pretty bad. So my code is this.
<template>
<search></search>
<table class="table table-condensed table-hover table-striped" v-infinite-scroll="loadMore" infinite-scroll-disabled="busy" infinite-scroll-distance="10">
<tbody id="trans-table">
<tr v-for="transaction in transactions" v-model="transactions">
<td v-for="item in transaction" v-html="item"></td>
</tr>
</tbody>
</table>
</template>
<script>
import Search from './Search.vue';
export default {
components: {
Search
},
data() {
return {
transactions: [],
currentPosition: 0
}
},
methods: {
loadMore() {
this.$root.$emit('loadMore', {
currentPosition: this.currentPosition
});
}
}
}
</script>
As You can see loadMore is triggered on infinite scroll and event is being sent to child component search. Well not just search but since it's root it's being broadcast to everyone.
What is better approach for this. I know that i should use props but I'm not sure how can i do that in this situation.
Just have a variable (call it moreLoaded) that you increment each time loadMore is called. Pass that and currentPosition to your search component as props. In Search, you can watch moreLoaded and take action accordingly.
Update
Hacky? My solution? Well, I never! ;)
You could also use a localized event bus. Set it up something like this:
export default {
components: {
Search
},
data() {
return {
bus: new Vue(),
transactions: [],
currentPosition: 0
}
},
methods: {
loadMore() {
this.bus.$emit('loadMore', {
currentPosition: this.currentPosition
});
}
}
}
and pass it to Search:
<search :bus="bus"></search>
which would take bus as a prop (of course), and have a section like
created() {
this.bus.$on('loadMore', (args) => {
// do something with args.currentPosition
});
}
I think that #tobiasBora's comment on #Roy J's answer is pretty important. If your component gets created and destroyed multiple times (like when using v-if) you will end with your handler being called multiple times. Also the handler will be called even if the component is destroyed (which can turn out to be really bad).
As #tobiasBora explains you have to use Vue's $off() function. This ended up being non trivial for me beacuse it needed a reference to the event handler function. What I ended up doing was define this handler as part of the component data. Notice that this must be an arrow function, otherwise you would need .bind(this) after your function definition.
export default {
data() {
return {
eventHandler: (eventArgs) => {
this.doSomething();
}
}
},
methods: {
doSomething() {
// Actually do something
}
},
created() {
this.bus.$on("eventName", this.eventHandler);
},
beforeDestroy() {
this.bus.$off("eventName", this.eventHandler);
},
}

In Vue.js can a component detect when the slot content changes

We have a component in Vue which is a frame, scaled to the window size, which contains (in a <slot>) an element (typically <img> or <canvas>) which it scales to fit the frame and enables pan and zoom on that element.
The component needs to react when the element changes. The only way we can see to do it is for the parent to prod the component when that happens, however it would be much nicer if the component could automatically detect when the <slot> element changes and react accordingly. Is there a way to do this?
To my knowledge, Vue does not provide a way to do this. However here are two approaches worth considering.
Watching the Slot's DOM for Changes
Use a MutationObserver to detect when the DOM in the <slot> changes. This requires no communication between components. Simply set up the observer during the mounted callback of your component.
Here's a snippet showing this approach in action:
Vue.component('container', {
template: '#container',
data: function() {
return { number: 0, observer: null }
},
mounted: function() {
// Create the observer (and what to do on changes...)
this.observer = new MutationObserver(function(mutations) {
this.number++;
}.bind(this));
// Setup the observer
this.observer.observe(
$(this.$el).find('.content')[0],
{ attributes: true, childList: true, characterData: true, subtree: true }
);
},
beforeDestroy: function() {
// Clean up
this.observer.disconnect();
}
});
var app = new Vue({
el: '#app',
data: { number: 0 },
mounted: function() {
//Update the element in the slot every second
setInterval(function(){ this.number++; }.bind(this), 1000);
}
});
.content, .container {
margin: 5px;
border: 1px solid black;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.13/vue.js"></script>
<template id="container">
<div class="container">
I am the container, and I have detected {{ number }} updates.
<div class="content"><slot></slot></div>
</div>
</template>
<div id="app">
<container>
I am the content, and I have been updated {{ number }} times.
</container>
</div>
Using Emit
If a Vue component is responsible for changing the slot, then it is best to emit an event when that change occurs. This allows any other component to respond to the emitted event if needed.
To do this, use an empty Vue instance as a global event bus. Any component can emit/listen to events on the event bus. In your case, the parent component could emit an "updated-content" event, and the child component could react to it.
Here is a simple example:
// Use an empty Vue instance as an event bus
var bus = new Vue()
Vue.component('container', {
template: '#container',
data: function() {
return { number: 0 }
},
methods: {
increment: function() { this.number++; }
},
created: function() {
// listen for the 'updated-content' event and react accordingly
bus.$on('updated-content', this.increment);
},
beforeDestroy: function() {
// Clean up
bus.$off('updated-content', this.increment);
}
});
var app = new Vue({
el: '#app',
data: { number: 0 },
mounted: function() {
//Update the element in the slot every second,
// and emit an "updated-content" event
setInterval(function(){
this.number++;
bus.$emit('updated-content');
}.bind(this), 1000);
}
});
.content, .container {
margin: 5px;
border: 1px solid black;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.13/vue.js"></script>
<template id="container">
<div class="container">
I am the container, and I have detected {{ number }} updates.
<div class="content">
<slot></slot>
</div>
</div>
</template>
<div id="app">
<container>
I am the content, and I have been updated {{ number }} times.
</container>
</div>
As far as I understand Vue 2+, a component should be re-rendered when the slot content changes. In my case I had an error-message component that should hide until it has some slot content to show. At first I had this method attached to v-if on my component's root element (a computed property won't work, Vue doesn't appear to have reactivity on this.$slots).
checkForSlotContent() {
let checkForContent = (hasContent, node) => {
return hasContent || node.tag || (node.text && node.text.trim());
}
return this.$slots.default && this.$slots.default.reduce(checkForContent, false);
},
This works well whenever 99% of changes happen in the slot, including any addition or removal of DOM elements. The only edge case was usage like this:
<error-message> {{someErrorStringVariable}} </error-message>
Only a text node is being updated here, and for reasons still unclear to me, my method wouldn't fire. I fixed this case by hooking into beforeUpdate() and created(), leaving me with this for a full solution:
<script>
export default {
data() {
return {
hasSlotContent: false,
}
},
methods: {
checkForSlotContent() {
let checkForContent = (hasContent, node) => {
return hasContent || node.tag || (node.text && node.text.trim());
}
return this.$slots.default && this.$slots.default.reduce(checkForContent, false);
},
},
beforeUpdate() {
this.hasSlotContent = this.checkForSlotContent();
},
created() {
this.hasSlotContent = this.checkForSlotContent();
}
};
</script>
There is another way to react on slot changes. I find it much cleaner to be honest in case it fits. Neither emit+event-bus nor mutation observing seems correct to me.
Take following scenario:
<some-component>{{someVariable}}</some-component>
In this case when someVariable changes some-component should react. What I'd do here is defining a :key on the component, which forces it to rerender whenever someVariable changes.
<some-component :key="someVariable">Some text {{someVariable}}</some-component>
Kind regard
Rozbeh Chiryai Sharahi
I would suggest you to consider this trick: https://codesandbox.io/s/1yn7nn72rl, that I used to watch changes and do anything with slot content.
The idea, inspired by how works VIcon component of vuetify, is to use a functional component in which we implement logic in its render function. A context object is passed as the second argument of the render function. In particular, the context object has a data property (in which you can find attributes, attrs), and a children property, corresponding to the slot (you could event call the context.slot() function with the same result).
Best regards
In Vue 3 with script setup syntax, I used the MutationObserver to great success:
<script setup>
import { ref, onMounted, onUnmounted } from 'vue';
const container = ref();
const mutationObserver = ref(null);
const mockData = ref([]);
const desiredFunc = () => {
console.log('children changed');
};
const connectMutationObserver = () => {
mutationObserver.value = new MutationObserver(desiredFunc);
mutationObserver.value.observe(container.value, {
attributes: true,
childList: true,
characterData: true,
subtree: true,
});
};
const disconnectMutationObserver = () => {
mutationObserver.value.disconnect();
};
onMounted(async () => {
connectMutationObserver();
setTimeout(() => { mockData.value = [1, 2, 3]; }, 5000);
});
onUnmounted(() => {
disconnectMutationObserver();
});
</script>
<template>
<div ref="container">
<div v-for="child in mockData" :key="child">
{{ child }}
</div>
</div>
</template>
My example code works better if the v-for is inside a slot that isn't visible to the component. If you are watching the list for changes, you can instead simply put a watcher on the list, such as:
watch(() => mockData.value, desiredFunc);
or if that doesn't work, you can use a deep watcher:
watch(() => mockData.value, desiredFunc, { deep: true });
My main goal is to highlight how to use the MutationObserver.
Read more here: https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver

Categories

Resources