Vue sibling components hooks lifecycle relationships - javascript

Never used Vue.js before. I have one parent component and 2 child components. These 2 child components should interact with asynchronous actions using the native Vue event bus system (with a dummy Vue object used as a shared container for the event bus).
Having something like the following:
EventBus.js
import Vue from "vue"
export default new Vue()
Parent.vue
import Child1 from "./Child1.vue"
import Child2 from "./Child2.vue"
export default {
name: "Parent",
components: {
child1: Child1,
child2: Child2,
}
}
Child1.vue
import EventBus from "./EventBus"
export default {
name: "Child1",
beforeCreate () {
EventBus.$once("MY_EVENT_X", async () => {
EventBus.$emit("MY_EVENT_Y")
})
},
mounted () {
// something
}
}
Child2.vue
import EventBus from "./EventBus"
export default {
name: "Child2",
beforeCreate () {
EventBus.$once("MY_EVENT_Y", async () => {
// do something
})
},
mounted () {
EventBus.$emit("MY_EVENT_X")
}
}
My question: having the events handlers defined in the "beforeCreate" hook, can I be sure that the "beforeCreate" hooks of both Child1 and Child2 components will be initiliazed before any of the "mounted" hooks of Child1 or Child2 will be called by Vue?

You can leverage component hook order between parent and children. When parent mounted is called, we will be sure all child components is created and mounted.
Image source from here
To do it, you can define a boolean flag in parent, change this flag to true in mounted hook:
import Child1 from "./Child1.vue"
import Child2 from "./Child2.vue"
export default {
name: "Parent",
components: {
child1: Child1,
child2: Child2,
},
data: () => ({
isChildMounted: false
}),
mounted() {
this.isChildMounted = true
}
}
Make sure to pass this flag to children component:
<child-2 :isReady="isChildMounted" />
Finally, in child component, watching for props change. When the flag is changed to true, we know all child components is ready. It's time to emit event.
import EventBus from "./EventBus"
export default {
name: "Child2",
props: ['isReady'],
beforeCreate () {
EventBus.$once("MY_EVENT_Y", async () => {
// do something
})
},
watch: {
isReady: function (newVal) {
if (newVal) {
// all child component is ready
EventBus.$emit("MY_EVENT_X")
}
}
}
}

Related

Add a class to a nabar modal in a component Vue.js

I wanted to know how can I add a class to a modal in a navbar components? My navbar is in App.vue and I wanted to create a message that would add the class "is-active" to a modal in my navbar when I click on it. But I can't find the way to do that..
Thank you
Usually when you have a parent -> child relationship you can use events. In this case since you have two components that are not linked (directly) then you have two alternatives.
Using store (it is usually used in cases where your application is of a considerate size)
You can use vuex to have a central place where you will have your global state. A simple example would be:
store/main.js
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
const store = new Vuex.Store({
state: {
isModalOpen: false
},
getters: {
isModalOpen => (state) => state.isModalOpen,
},
mutations: {
setIsModalOpen (state, isOpen) {
state.isModalOpen = isOpen;
}
}
})
then you can access the store in your component as such:
<template>
<navbar :class="[isNavBarOpen ? "is-active" : ""]" />
</template>
export default {
computed: {
isNavBarOpen () {
this.$store.getters['isModalOpen']
}
}
}
Event bus (it is usually used in cases where you have a small app and do not need a global state manager)
Read more about EventBus here.
You can create a simple EventBus
services/eventBus.js
import Vue from 'vue';
const export EventBus = new Vue();
then on your component when the modal is open you can do:
// # -> is an alias to your root folder. Most projects scafolded by Vue CLI has this by default
import {EventBus} from "#/services/eventBus"
export default {
methods: {
openStore: () => {
// your logic to open modal
EventBus.$emit('modal-open');
}
}
}
then on your App.vue you just listen to this event
App.vue
<template>
<navbar :class="[isModalOpen ? "is-active" : ""]" />
</template>
// # -> is an alias to your root folder. Most projects scafolded by Vue CLI has this by default
import {EventBus} from "#/services/eventBus"
export default {
data() {
return {
isModalOpen: false,
}
},
created() {
EventBus.$on('modal-open', this.onModalOpen);
},
methods: {
onModalOpen() {
this.isModalOpen = true;
}
}
}
The one you will pick depends on our application structure and if you think it is complex enough to use a central state management (vuex).
There might contain some errors in the code but the main idea is there.

VueJS $emit and $on not working in one component to another component page

I am trying to long time Vue $emit and $on functionality but still I am not getting any solution. I created simple message passing page its two pages only. One is Component Sender and Component Receiver and added eventbus for $emit and $on functionality.
I declared $emit and $on functionality but I don't know where I made a mistake.
please help some one.
Component Sender:
<script>
import { EventBus } from '../main';
export default {
name: 'Send',
data () {
return {
text: '',
receiveText: ''
}
},
methods: {
sender() {
EventBus.$emit('message', this.text);
this.$router.push('/receive');
}
}
}
</script>
Component Receiver:
<script>
import { EventBus } from '../main';
export default {
name: 'Receive',
props: ["message"],
data () {
return {
list: null
}
},
created() {
EventBus.$on('message', this.Receive);
},
methods: {
Receive(text){
console.log('text', text);
this.list = text;
},
save() {
alert('list', this.list);//need to list value but $emit is not working here
}
}
}
</script>
Router View:
const router = new Router({
mode: 'history',
routes: [
{
path: "/",
name: "Send",
component: Send
},
{
path: "/receive",
name: "Receive",
component: Receive,
props: true
}
]
})
Main.JS
import Vue from 'vue'
import App from './App.vue'
import router from './router';
export const EventBus = new Vue();
new Vue({
el: '#app',
router,
render: h => h(App)
})
The EventBus is designed to allow communication between two components that exist on the page at the same time. If you need data that persists between router-views, then you should look at using a Vuex store.
As it is, the Receiver component doesn't exist at the time of the message being sent, and therefore it has no listener attached to the event.
Well your mistake is here:
created() {
EventBus.$on('message', this.Receive);
},
the second argument is an handler it should look like this:
created() {
EventBus.$on('message', function(data){
this.Receive(data);
console.log(data)
});
},
you should see now 2 console.log() messages

How do you unit test a Vue.js functional component with a render function that returns any array of elements?

In Vue.js, a functional component can return multiple root nodes by using a render function that returns an array of createdElements.
export default {
functional: true,
props: ['cellData'],
render: function (h, context) {
return [
h('td', context.props.cellData.category),
h('td', context.props.cellData.description)
]
}
}
This works great but I'm having trouble trying to create a unit test for such a component. Using shallowMount on the component results in [Vue warn]: Multiple root nodes returned from render function. Render function should return a single root node.
import { shallowMount } from '#vue/test-utils'
import Cell from '#/components/Cell'
wrapper = shallowMount(Cell, {
context: {
props: {
cellData {
category: 'foo',
description: 'bar'
}
}
}
});
This github issue suggests that the component needs to be wrapped in a single root node to actually render it, but trying that results in [vue-test-utils]: mount.context can only be used when mounting a functional component
import { shallowMount } from '#vue/test-utils'
import Cell from '#/components/Cell'
wrapper = shallowMount('<div><Cell></div>', {
context: {
props: {
cellData {
category: 'foo',
description: 'bar'
}
}
}
});
So how do I test a functional component that returns multiple root nodes?
You could create a higher order, transparent wrapper component that passes all props and event listeners to the inner Cell component using v-bind="$attrs"[1] and v-on="$listeners"[2]. Then you can use propsData to pass props to the wrapper component ..
import { mount } from '#vue/test-utils'
import Cell from '#/components/Cell'
const WrappedCell = {
components: { Cell },
template: `
<div>
<Cell v-bind="$attrs" v-on="$listeners" />
</div>
`
}
const wrapper = mount(WrappedCell, {
propsData: {
cellData: {
category: 'foo',
description: 'bar'
}
}
});
You can create a fragment_wrapper for wrapping your Components with Fragments (multiple root elements).
//File: fragment_wrapper.js
exports.fragment_wrapper = function(FragmentComponent){
const wrapper = {
components: { FragmentComponent },
props: FragmentComponent.props,
template: `<div><FragmentComponent v-bind="$props" v-on="$listeners"/></div>`
}
return wrapper;
}
Then you can use this to test all your Fragmented Components as follows:
import { mount } from '#vue/test-utils'
import { fragment_wrapper } from './fragment_wrapper'
import Cell from './components/Cell'
describe('Test Cell', () => {
let WrappedCell = fragment_wrapper(Cell);
const wrapper = mount(WrappedCell, {
propsData: {
cellData: {
category: 'foo',
description: 'bar'
}
}
});
it('renders the correct markup', () => {
expect(wrapper.html()).toContain('<td>foo</td>')
});
});

Unable to overwrite method in Vue constructor

I have a component with one method, which I'm firing on creation. It's using vue-select but purpose of this component shouldn't be relevant to my issue.
<template>
<v-select :on-change="onchangecallback"></v-select>
</template>
<script>
import Vue from 'vue'
import vSelect from 'vue-select'
Vue.component('v-select', vSelect);
export default {
methods: {
onchangecallback: () => {alert('default')}
},
created: function() {
this.onchangecallback();
}
}
</script>
In other file I'm importing this component and creating a new instance of it with Vue constructor and passing new onchangecallback method, which, by my understanding, should overwrite the default onchangecallback method:
import VSelect from './components/ui/VSelect.vue';
new Vue({
VSelect,
el: '#app',
components: {VSelect},
template: `<v-select />`,
methods: {
onchangecallback: () => {alert('custom')} // doesn't work
}
});
But when I start the app, instead of alert('custom') I still get alert('default').
I don't know what you are trying to achieve.
But here is my solution https://codesandbox.io/s/84qw9z13v9
You need to define a prop to pass your new callback function to that component (through the prop)
props: {
onchangecallback: {
type: Function,
default() {
return function() {
alert('default');
};
},
},
},
created: function() {
this.onchangecallback();
}
And the use it instead the default one.
Check all the code in that snippet.
For communication between components you're supposed to use events. Emit an event in a child component and make the parent component listen to it:
In the child component:
created() {
this.$emit('change')
}
In the parent component:
Template:
<child-component v-on:change="doSomething" />
Methods:
methods: {
doSomething() {
// ...
}
}
Explanation
In the child component you emit an event, in this case "change", whenever something changed. In your case this was upon creation of the component.
In the parent component you tell Vue that, whenever the child component emits a "change" event, it should run the method "doSomething" in your parent component.
This is used in many places, i.e. inputs, where the input emits an event "input" whenever its content changes and any parent can listen for that by using v-on:input="methodHere".

Child component to use parent function in vue js

I have a method initialized within the parent component called setMessage() and I'd like to be able to call it within the child component.
main.js
const messageBoard = new Vue({
el: '#message-board',
render: h => h(App),
})
App (App.vue (parent))..
export default {
data() {
return { messages: state }
},
methods: {
setMessage(message) {
console.log(message);
}
},
template: `
<div>
<child-component></child-component>
</div>
`,
}
child
const child = Vue.extend({
mounted() {
// attempting to use this function from the parent
this.$dispatch('setMessage', 'HEY THIS IS MY MESSAGE!');
}
});
Vue.component('child-component', child);
Right now I'm getting this.$dispatch is not a function error message. What am I doing wrong? How can I make use of parent functions in various child components? I've also tried $emit, it doesn't throw an error & it doesn't hit the function.
Thank you for your help in advance!
You have a couple options.
Option 1 - referencing $parent from child
The simplest is to use this.$parent from your child component. Something like this:
const Child = Vue.extend({
mounted() {
this.$parent.setMessage("child component mounted");
}
})
Option 2 - emitting an event and handling in parent
But that strongly couples the child to its parent. To fix this, you could $emit an event in the child and have the parent handle it. Like this:
const ChildComponent = Vue.extend({
mounted() {
this.$emit("message", "child component mounted (emitted)");
}
})
// in the parent component template
<child-component #message="setMessage"></child-component>
Option 3 - central event bus
One last option, if your components don't have a direct parent-child relationship, is to use a separate Vue instance as a central event bus as described in the Guide. It would look something like this:
const bus = new Vue({});
const ChildComponent = Vue.extend({
mounted() {
bus.$emit("message-bus", "child component mounted (on the bus)");
}
})
const app = new Vue({
...
methods: {
setMessage(message) {
console.log(message);
}
},
created() {
bus.$on('message-bus', this.setMessage)
},
destroyed() {
bus.$off('message-bus', this.setMessage)
}
});
Update (Option 2a) - passing setMessage as a prop
To follow up on your comment, here's how it might work to pass setMessage to the child component as a prop:
const ChildComponent = Vue.extend({
props: ["messageHandler"],
mounted() {
this.messageHandler('from message handler');
}
})
// parent template (note the naming of the prop)
<child-component :message-handler="setMessage"></child-component>
// parent component providing 'foo'
var Provider = {
methods: {
foo() {
console.log('foo');
},
},
provide: {
foo: this.foo,
},
}
// child component injecting 'foo'
var Child = {
inject: ['foo'],
created() {
this.foo() // => "foo";
},
}

Categories

Resources