Event from parent to child component - javascript

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);
},
}

Related

Wrapping a JS library in a Vue Component

I need to know which is best practice for wrapping a JS DOM library in Vue. As an example I will use EditorJS.
<template>
<div :id="holder"></div>
</template>
import Editor from '#editorjs/editorjs'
export default {
name: "editorjs-wrapper",
props: {
holder: {
type: String,
default: () => {return "vue-editor-js"},
required: true
}
},
data(){
return {
editor: null
};
},
methods: {
init_editor(){
this.editor = new Editor({
holder: this.holder,
tools: {
}
});
}
},
mounted(){
this.init_editor();
},
//... Destroy on destroy
}
First:
Supose I have multiple instances of <editorjs-wrapper> in the same View without a :hook then all intances would have the same id.
<div id="app">
<editorjs-wrapper></editorjs-wrapper>
<editorjs-wrapper></editorjs-wrapper>
</div>
Things get little weird since they both try to process the DOM #vue-editor-js. Would it be better if the component generated a random id as default?
Second:
EditorJS provides a save() method for retrieving its content. Which is better for the parent to be able to call save() method from the EditorJS inside the child?
I have though of two ways:
$emit and watch (Events)
// Parent
<div id="#app">
<editorjs-wrapper #save="save_method" :save="save">
</div>
// Child
...
watch: {
save: {
immediate: true,
handler(new_val, old_val){
if(new_val) this.editor.save().then(save=>this.$emit('save', save)) // By the way I have not tested it it might be that `this` scope is incorrect...
}
}
}
...
That is, the parent triggers save in the child an thus the child emits the save event after calling the method from the EditorJS.
this.$refs.childREF
This way would introduce tight coupling between parent and child components.
Third:
If I want to update the content of the child as parent I don't know how to do it, in other projects I have tried without success with v-modal for two way binding:
export default{
name: example,
props:{
content: String
},
watch:{
content: {
handler(new_val, old_val){
this.update_content(new_val);
}
}
},
data(){
return {
some_js_framework: // Initialized at mount
}
},
methods: {
update_content: function(new_val){
this.some_js_framework.update(new_val)
},
update_parent: function(new_val){
this.$emit('input', this.some_js_framework.get_content());
}
},
mounted(){
this.some_js_framework = new Some_js_framework();
this.onchange(this.update_parent);
}
}
The problem is:
Child content updated
Child emit input event
Parent updates the two-way bidding of v-model
Since the parent updated the value the child watch updates and thus onchange handler is triggered an thus 1. again.

Vue2: warning: Avoid mutating a prop directly

I'm stuck in the situation where my child component (autocomplete) needs to update a value of its parent (Curve), And the parent needs to update the one of the child (or to completely re-render when a new Curve component is used)
In my app the user can select a Curve in a list of Curve components. My previous code worked correctly except the component autocomplete was not updated when the user selected another Curve in the list (the component didn't update its values with the value of the parent).
This problem is fixed now but I get this warning:
Avoid mutating a prop directly since the value will be overwritten
whenever the parent component re-renders. Instead, use a data or
computed property based on the prop's value. Prop being mutated:
"value"
The description of this warning explain exactly what behavior I expect from my components. Despite this warning, this code works perfectly fine !
Here is the code (parts of it have been removed to simplify)
// curve.vue
<template>
<autocomplete v-model="curve.y"></autocomplete>
</template>
<script>
import Autocomplete from './autocomplete'
export default {
name: 'Curve',
props: {
value: Object
},
computed: {
curve() { return this.value }
},
components: { Autocomplete }
}
</script>
// autocomplete.vue
<template>
<input type="text" v-model="content"/>
</template>
<script>
export default {
name: 'Autocomplete',
props: {
value: {
type: String,
required: true
}
},
computed: {
content: {
get() { return this.value },
set(newValue) { this.value = newValue }
}
}
}
</script>
A lot of people are getting the same warning, I tried some solutions I found but I was not able to make them work in my situation (Using events, changing the type of the props of Autocomplete to be an Object, using an other computed value, ...)
Is there a simple solution to solve this problem ? Should I simply ignore this warning ?
you can try is code, follow the prop -> local data -> $emit local data to prop flow in every component and component wrapper.
ps: $emit('input', ...) is update for the value(in props) bind by v-model
// curve.vue
<template>
<autocomplete v-model="curve.y"></autocomplete>
</template>
<script>
import Autocomplete from './autocomplete'
export default {
name: 'Curve',
props: {
value: Object
},
data() {
return { currentValue: this.value }
}
computed: {
curve() { return this.currentValue }
},
watch: {
'curve.y'(val) {
this.$emit('input', this.currentValue);
}
},
components: { Autocomplete }
}
</script>
// autocomplete.vue
<template>
<input type="text" v-model="content"/>
</template>
<script>
export default {
name: 'Autocomplete',
props: {
value: {
type: String,
required: true
}
},
data() {
return { currentValue: this.value };
},
computed: {
content: {
get() { return this.value },
set(newValue) {
this.currentValue = newValue;
this.$emit('input', this.currentValue);
}
}
}
}
</script>
You can ignore it and everything will work just fine, but it's a bad practice, that's what vue is telling you. It'll be much harder to debug code, when you're not following the single responsibility principle.
Vue suggests you, that only the component who owns the data should be able to modify it.
Not sure why events solution ($emit) does not work in your situation, it throws errors or what?
To get rid of this warning you also can use .sync modifier:
https://v2.vuejs.org/v2/guide/components.html#sync-Modifier

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

Setting props for components using v-for in Vue JS

I have a PhoneCard.vue component that I'm trying to pass props to.
<template>
<div class='phone-number-card'>
<div class='number-card-header'>
<h4 class="number-card-header-text">{{ cardData.phone_number }}</h4>
<span class="number-card-subheader">
{{ cardData.username }}
</span>
</div>
</div>
</template>
<script>
export default {
props: ['userData'],
components: {
},
data() {
return {
cardData: {}
}
},
methods: {
setCardData() {
this.cardData = this.userData;
console.log(this.cardData);
}
},
watch: {
userData() {
this.setCardData();
}
}
}
The component receives a property of userData, which is then being set to the cardData property of the component.
I have another Vue.js component that I'm using as a page. On this page I'm making an AJAX call to an api to get a list of numbers and users.
import PhoneCard from './../../global/PhoneCard.vue';
export default {
components: {
'phone-card': PhoneCard
},
data() {
return {
phoneNumbers: [],
}
},
methods: {
fetchActiveNumbers() {
console.log('fetch active num');
axios.get('/api').then(res => {
this.phoneNumbers = res.data;
}).catch(err => {
console.log(err.response.data);
})
}
},
mounted() {
this.fetchActiveNumbers();
}
}
Then once I've set the response data from the ajax call equal to the phoneNumbers property.
After this comes the issue, I try to iterate through each number in the phoneNumber array and bind the value for the current number being iterated through to the Card's component, like so:
<phone-card v-for="number in phoneNumbers" :user-data="number"></phone-card>
However this leads to errors in dev tools such as property username is undefined, error rendering component, cannot read property split of undefined.
I've tried other ways to do this but they all seem to cause the same error. any ideas on how to properly bind props of a component to the current iteration object of a vue-for loop?
Try
export default {
props: ['userData'],
data() {
return {
cardData: this.userData
}
}
}
Answered my own question, after some tinkering.
instead of calling a function to set the data in the watch function, all I had to do was this to get it working.
mounted() {
this.cardData = this.userData;
}
weird, I've used the watch method to listen for changes to the props of components before and it's worked flawlessly but I guess there's something different going on here. Any insight on what's different or why it works like this would be cool!

Add Vue.js event on window

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.

Categories

Resources