Vuejs DOM not updating if the event is fired from $on method - vue.js

I'm guessing that it has to do something with Vue not detecting the changes in the tracks object.
mounted(){
Event.$emit('requestCurrentTrack');
Event.$on('currentSong', (data) => this.fetchAlbum(data)); //Data from this method won't output on the screen.
this.fetchAlbum(); // Data from this method out will output to the screen
},
methods:{
fetchAlbum(){
axios.get('/api/album/'+this.id).then((response)=>{
this.album = response.data[1][0];
this.tracks = response.data[0];
this.artistName = this.tracks[0].artist;
});
},
play(data, index){
if(data){
Event.$emit('playTrack', data, index);
}
}
}

You have to learn more about event bus.Take a look to this article
To use event bus take a look below:
In main.js you have to create the event bus
import Vue from 'vue'
import App from './App.vue'
export const eventBus = new Vue() //creating the event bus
new Vue({
el: '#app',
render: h => h(App)
})
A rendered component,lets name it childOne.vue:
<template>
<div>
<button #click="clicked">Click Me</button>
</div>
</template>
<script>
import { eventBus } from '../main'
export default {
name: 'child-one',
methods: {
clicked () {
eventBus.$emit('eventName', 'text passed through event bus') //creating the event with the name eventName and pass a text
}
}
}
</script>
Another rendered component lets name it childTwo.vue
<template>
<div>
<!-- some html here -->
</div>
</template>
<script>
import { eventBus } from '../main'
export default {
name: 'child-two',
created() {
eventBus.$on('eventName', dataPassed => { //listening to event with name eventName
console.log(dataPassed)
})
}
}
</script>
Note the eventBus will work only if your components are rendered.So to make this example to work you can do it by importing the two components in App.vue and registrering them

Related

Watch child properties from parent component in vue 3

I'm wondering how I can observe child properties from the parent component in Vue 3 using the composition api (I'm working with the experimental script setup).
<template>//Child.vue
<button
#click="count++"
v-text="'count: ' + count"
/>
</template>
<script setup>
import { ref } from 'vue'
let count = ref(1)
</script>
<template>//Parent.vue
<p>parent: {{ count }}</p> //update me with a watcher
<Child ref="childComponent" />
</template>
<script setup>
import Child from './Child.vue'
import { onMounted, ref, watch } from 'vue'
const childComponent = ref(null)
let count = ref(0)
onMounted(() => {
watch(childComponent.count.value, (newVal, oldVal) => {
console.log(newVal, oldVal);
count.value = newVal
})
})
</script>
I want to understand how I can watch changes in the child component from the parent component. My not working solution is inspired by the Vue.js 2 Solution asked here. So I don't want to emit the count.value but just watch for changes.
Thank you!
The Bindings inside of <script setup> are "closed by default" as you can see here.
However you can explicitly expose certain refs.
For that you use useContext().expose({ ref1,ref2,ref3 })
So simply add this to Child.vue:
import { useContext } from 'vue'
useContext().expose({ count })
and then change the Watcher in Parent.vue to:
watch(() => childComponent.value.count, (newVal, oldVal) => {
console.log(newVal, oldVal);
count.value = newVal
})
And it works!
I've answered the Vue 2 Solution
and it works perfectly fine with Vue 3 if you don't use script setup or explicitly expose properties.
Here is the working code.
Child.vue
<template>
<button #click="count++">Increase</button>
</template>
<script>
import { ref } from 'vue';
export default {
setup() {
return {
count: ref(0),
};
},
};
</script>
Parent.vue
<template>
<div id="app">
<Child ref="childComponent" />
</div>
</template>
<script>
import { ref, onMounted, watch } from 'vue';
import Child from './components/Child.vue';
export default {
components: {
Child,
},
setup() {
const childComponent = ref(null);
onMounted(() => {
watch(
() => childComponent.value.count,
(newVal) => {
console.log({ newVal }) // runs when count changes
}
);
});
return { childComponent };
},
};
</script>
See it live on StackBlitz
Please keep reading
In the Vue 2 Solution I have described that we should use the mounted hook in order to be able to watch child properties.
In Vue 3 however, that's no longer an issue/limitation since the watcher has additional options like flush: 'post' which ensures that the element has been rendered.
Make sure to read the Docs: Watching Template Refs
When using script setup, the public instance of the component it's not exposed and thus, the Vue 2 solutions will not work.
In order to make it work you need to explicitly expose properties:
With script setup
import { ref } from 'vue'
const a = 1
const b = ref(2)
defineExpose({
a,
b
})
With Options API
export default {
expose: ['publicData', 'publicMethod'],
data() {
return {
publicData: 'foo',
privateData: 'bar'
}
},
methods: {
publicMethod() {
/* ... */
},
privateMethod() {
/* ... */
}
}
}
Note: If you define expose in Options API then only those properties will be exposed. The rest will not be accessible from template refs or $parent chains.

Hey! Do you know how to dynamically change the layout in Nuxt without store?

There is my default layout.
I want to change visibility on my header, when my modal is open, but I don't know how to change it dynamically. Any ideas?
// default.vue
<template>
<div class="container">
<header class="default-header">
<router-link class="logo" to="/"></router-link>
<div class="button-group">
<router-link to="/login" class="btn">Log in</router-link>
<router-link to="/register" class="btn">Sign up</router-link>
</div>
</header>
<nuxt />
</div>
</template>
//index.vue
<template>
<div>
<div #click="openModal">Open Modal</div>
<modal-popup v-model="showModal"></modal-popup>
</div>
</template>
<script>
import ModalPopup from "~/components/ModalPopup";
export default {
name: "Login",
components: {
ModalPopup
},
data() {
return {
showModal: false;
}
},
methods() {
openModal() {
this.showModal = true;
},
closeModal() {
this.showModal = false;
},
}
};
</script>
To do it without a store you can create an EventBus to trigger events which you can listen to. An eventbus provides communication between vue components.
You can create an eventbus js file.
import Vue from 'vue';
const EventBus = new Vue();
export default EventBus;
Then in your components you can import it
import EventBus from '/eventbus'
In the index.vue component in your openModal() function you can change it to trigger the open modal event like this
openModal() {
EventBus.$emit('modal-opened', true);
this.showModal = true;
}
then in your default.vue component you can add a listener in the mounted function
mounted() {
EventBus.$on(‘modal-open’, function (payLoad) {
// Change header visibility here
});
}
Extra Note
If you don't want to import the Eventbus all the time you can import Eventbus in your app.js and just before your new Vue() you can add bus to the vue properties so your file will look something like this
import Vue from 'vue'
import EventBus from '/eventbus'
Object.defineProperties(
Vue.prototype,
{
$bus: {
get() => { return EventBus; }
}
}
)
new Vue({})
Then you can access the bus in your components like this
this.$bus.$emit('modal-open', true);
and
this.$bus.$on('modal-open', function(payload) {
})
Hope that helps

vue js passing data through global event bus not working

Using vue cli I have created a simple vue app with two nested components. I want to pass data between them clicking the h1 tag in my component 1 (a more structured approach suggests to use vuex but this is a very easy app passing simple data using for test).
Clicking the h1 I receive an error but I'm not getting the point. The error says
[Vue warn]: Error in event handler for "titleChanged": "TypeError: Cannot read property 'apply' of undefined"
(found in <Root>)
My code is below
main.js
import Vue from 'vue'
import App from './App.vue'
import Axios from 'axios'
Vue.config.productionTip = false
Vue.prototype.$http = Axios
export const bus = new Vue();
new Vue({
render: h => h(App),
}).$mount('#app')
app.vue
<template>
<div>
<comp-1 v-bind:title="title"></comp-1>
<comp-2 v-bind:title="title"></comp-2>
</div>
</template>
<script>
import comp-1 from './components/Comp-1.vue'
import comp-2 from './components/Comp-2.vue'
export default {
components: {
'comp-1': comp-1,
'comp-2': comp-2
},
data() {
return {
title: "my title"
}
}
}
</script>
<style scoped>
</style>
comp-1.vue
<template>
<header>
<h1 v-on:click="changeTitle">{{ pTitle }}</h1>
</header>
</template>
<script>
import {bus} from '../main'
export default {
props: {
title: {
Type: String
}
},
data() {
return {
pTitle: ''
}
},
created: function() {
this.pTitle = this.title
},
methods: {
changeTitle: function() {
this.pTitle = 'I have changed my title!'
bus.$emit('titleChanged', this.pTitle)
}
}
}
</script>
<style scoped>
</style>
comp-2.vue
<template>
<footer>
<p>{{ title }}</p>
</footer>
</template>
<script>
import {bus} from '../main'
export default {
props: {
title: {
Type: String
}
},
data() {
return {
pTitle: ''
}
},
created() {
this.pTitle = this.title;
bus.$on('titleChanged'), (data) => {
this.title = data
}
}
}
</script>
<style scoped>
</style>
In created of comp-2 component, there is a mistake in event handler
Change it like this:
bus.$on("titleChanged", data => {
this.title = data;
});

Pass data from one component to all with $emit without using #click in VueJS

Trying to learn vuejs I got to the question how to pass any data from one component to all, using $emit but without using any #click.
It is possible some how that the data to be just available and grab it any time, without using the click?
Let's say we have this example with normal #click and $emit.
main.js
export const eventBus = new Vue()
Hello.vue
<template>
<div>
<h2>This is Hello component</h2>
<button
#click="emitGlobalClickEvent()">Click me</button>
</div>
</template>
<script>
import { eventBus } from '../main'
export default {
data () {
return {
msg: 'Welcome to Your Vue.js App'
}
},
methods: {
emitGlobalClickEvent () {
eventBus.$emit('messageSelected', this.msg)
}
}
}
</script>
User.vue
<template>
<div>
<h2>This is User component</h2>
<user-one></user-one>
</div>
</template>
<script>
import { eventBus } from '../main'
import UserOne from './UserOne.vue'
export default {
created () {
eventBus.$on('messageSelected', msg => {
console.log(msg)
})
},
components: {
UserOne
}
}
</script>
UserOne.vue
<template>
<div>
<h3>We are in UserOne component</h3>
</div>
</template>
<script>
import { eventBus } from '../main'
export default {
created () {
eventBus.$on('messageSelected', msg => {
console.log('From UserOne message !!!')
})
}
}
</script>
I want to get this message : Welcome to Your Vue.js App from Hello.vue in all components, but without #click, if is possible.
You can create another Javascript file which holds an Object with your initial state. Similar to how you define data in your components.
In this file your export your Object and import it in all Components which need access to this shared state. Something along the lines of this:
import Store from 'store';
data() {
return {
store
}
}
This might help:
https://v2.vuejs.org/v2/guide/state-management.html
At this point if you app grows even more in complexity you might also start checking out Vuex which helps to keep track of changes(mutations) inside of your store.
The given example is essential a very oversimplified version of Vuex.

How to catch events across multiple child Vue components

I am building a form framework in vue. I have components for each field type. Each field type component uses this.$emit to communicate changes with the parent component.
I am able to trigger events in the parent component using v-on directives as follows:
<template>
<div v-if="fieldsLoaded">
<form-select :field="fields.title" v-on:updated="validate" ></form-select>
<form-input :field="fields.first_name" v-on:updated="validate" ></form-input>
</div>
</template>
However, I don't want to have to manually specify that every component should trigger the validate method individually.
How can I have the parent component listen for the updated emit across all its child components?
Edit: I'm looking for something like the below, though $on only catches emits that occur within the same component, rather than its children
created: function(){
this.$on('updated',validate)
}
The best way is to use event bus or even better in my opinion vuex.
For the first case take a look here
For the second here
With event bus you can emit an event, and listen to that event whenever you want(at parent,child even in the same component)
Vuex It serves as a centralized store for all the components in an application and you can have properties in that store,and you can use and manipulate them.
Example with event Bus:
main.js:
import Vue from 'vue'
import App from './App.vue'
export const eventBus = new Vue();
new Vue({
el: '#app',
render: h => h(App)
})
User Component
<template>
<button #click="clicked">Click me to create event</button>
</template>
<script>
import { eventBus } from './main'
export default {
name: 'User',
methods: {
clicked() {
eventBus.$emit('customEvent', 'a text to pass')
}
}
}
</script>
Admin component
<template>
<p>The message from event is: {{message}}</p>
</template>
<script>
import { eventBus } from './main'
export default {
name: 'Admin',
data: () => ({
message: ''
})
created() {
eventBus.$on('customEvent', dataPassed => {
this.message = dataPassed
}
}
}
</script>
Take a look to this tutorial to learn Vuex
For your case you can use v-model like following:
<template>
<div v-if="fieldsLoaded">
<form-select v-model="fields.title" :validate="validate" ></form-select>
<form-input v-model="fields.first_name" :validate="validate" ></form-input>
</div>
</template>
v-model is essentially syntax sugar for updating data on user input events.
<input v-model="something">
is just syntactic sugar for:
<input v-bind:value="something" v-on:input="something = $event.target.value">
You can pass a prop : value in the child components, and before changing input field call a function to validate which is also passed as a prop.
Vue.component('form-select', {
props: ['options', 'value', 'onChange', 'validate'], //Added one more prop
template: '#your-template',
mounted: function () {
},
methods: {
change (opt) {
if (this.validate !== undefined) {
var isValid = this.validate(this.value)
if(!isValid) return;
}
this.$emit('input', opt)
},
},
})