Vuejs watcher not reacting to prop's change - vue.js

I have a simple .vue component in which there is one prop request and a watcher to watch this prop. If request is true then I call a method.
The problem is, my watcher is not reacting to the changes.
I have tried to make the code below work.
foobar.vue
<template><div> ... </div></template>
<script>
export default {
// ...
props: {
request: {
type: Boolean,
required: false,
default: false
}
},
watch: {
request (state) {
if(state) {
// run some method
}
}
}
}
</script>
I now call the component as (after requiring it)
<template>
<foobar :request="access"> </foobar>
<button #click="access = !access"> {{ access ? 'Turn off' : 'Turn on'}} </button>
</template>
<script>
export default {
data () {
return { access: false }
},
components: { foobar }
}
</script>

Set the watcher to immediate.
watch: {
request :
immediate:true,
handler(state) {
if(state){
// run some method
}
}
}
}
}

Related

Vuejs 3 Stuck at loading page

Hello I have a problem showing/loading page. Some routes are working, and those routes that work are calling API for data, the ones that not work are not calling API for data.
So I am calling API for data(I don't have to, and I don't want to), and then the page loads.
I am watching the selected language computed:{lang(){...}} because in some situations I need to get translated data from API.
Here is the code with added API call.
asyncDataStatus.js
export default{
data(){
return{
asyncDataStatus_ready: false
}
},
methods: {
asyncDataStatus_fetched(){
this.asyncDataStatus_ready = true
this.$emit('ready')
}
},
}
App.vue
<template>
<router-view v-show="showPage" #ready="onPageReady" />
</template>
<script>
export default {
import asyncDataStatus from '#/mixins/asyncDataStatus'
export default {
methods:{
onPageReady(){
this.showPage = true
NProgress.done()
}
},
mixins: [asyncDataStatus],
created(){
NProgress.configure({
speed: 200,
spinner: false
})
this.$router.beforeEach(() => {
this.showPage = false
NProgress.start()
})
}
}
</script>
Contact.vue
<template>
<router-view v-show="showPage" #ready="onPageReady" />
</template>
<script>
import store from '../store';
import asyncDataStatus from '#/mixins/asyncDataStatus'
export default {
mixins: [asyncDataStatus],
computed: {
lang() {
return this.$i18n.locale
},
watch: {
async lang(newLang, oldLang) {
if(newLang !== oldLang) {
// If I remove this line `store.dispatch` line (which is not needed in this file), then `this.asyncDataStatus_fetched()` won't happen
await store.dispatch('impressionModule/getImpressions')
this.asyncDataStatus_fetched()
}
},
created() {
this.asyncDataStatus_fetched()
}
}
}
</script>

Is it possible to watch injected property

I am building an application which is using Vue 3 and I am providing a property in a parent component which I am subsequently injecting into multiple child components. Is there any way for a component which gets injected with this property to watch it for changes?
The parent component looks something like:
<template>
<child-component/>
<other-child-component #client-update="update_client" />
</template>
<script>
export default {
name: 'App',
data() {
return {
client: {}
}
},
methods: {
update_client(client) {
this.client = client
}
},
provide() {
return {
client: this.client
}
},
}
</script>
The child component looks like:
<script>
export default {
name: 'ChildComponent',
inject: ['client'],
watch: {
client(new_client, old_client) {
console.log('new client: ', new_client);
}
}
}
</script>
I am trying to accomplish that when the provided variable gets updated in the parent the children components where its being injected should get notified. For some reason the client watch method is not getting called when client gets updated.
Is there a better way of accomplishing this?
Update
After further testing I see that there is a bigger issue here, in the child component even after the client has been updated in the parent, the client property remains the original empty object and does not get updated. Since the provided property is reactive all places it is injected should automatically be updated.
Update
When using the Object API reactive definition (data(){return{client:{}}), even though the variable is reactive within the component, the injected value will be static. This is because provide will set it to the value that it is initially set to. To have the reactivity work, you will need to wrap it in a computed
provide(){
return {client: computed(()=>this.client)}
}
docs:
https://vuejs.org/guide/components/provide-inject.html#working-with-reactivity
You may also need to use deep for your watch
Example:
<script>
export default {
name: 'ChildComponent',
inject: ['client'],
watch: {
client: {
handler: (new_client, old_client) => {
console.log('new client: ', new_client);
},
deep: true
}
}
}
</script>
As described in official documentation ( https://v2.vuejs.org/v2/api/#provide-inject ), by default, provide and inject bindings are not reactive. But if you pass down an observed object, properties on that object remain reactive.
For objects, Vue cannot detect property addition or deletion. So the problem in your code might be here:
data() {
return {
client: {}
}
},
Since you change the client property of this object ( this.client.client = client ), you should declare this key in data, like this:
data() {
return {
client: { client: null }
}
},
Now it becomes reactive.
I did a code sandbox reproducing your code watching an injected property: https://codesandbox.io/s/vue-inject-watch-ffh2b
For some reason the only way I got this to work was by only updating properties of the initial injected object instead of replacing the whole object. I also was not able to get watch working with the injected property despite setting deep: true.
Updated parent component:
<template>
<child-component/>
<other-child-component #client-update="update_client" />
</template>
<script>
export default {
name: 'App',
data() {
return {
client: {}
}
},
methods: {
update_client(client) {
this.client.client = client
}
},
provide() {
return {
client: this.client
}
},
}
</script>
Updated child component:
<template>
<button #click="get_client">Get client</button>
</template>
<script>
export default {
name: 'ChildComponent',
inject: ['client'],
methods: {
get_client() {
console.log('updated client: ', client);
}
}
}
</script>
create a new value and reference the value from inject into it
inject: ['client'],
data: () => ({
value: null,
}),
created() {
this.value = this.client;
},
watch: {
value: {
handler() {
/* ... */
},
deep: true,
}
}
Now you can watch the value.
Note: "inject" must be an object
I ran into the same issue. But i just had to look more closely for details in the docs to make it work. In the end everything worked fine for me.
I built a vue plugin providing a Map together with some function as a readonly ref. Then it starts changing the Map contents once a second:
plugin.js
import { ref, readonly } from 'vue';
const rRuns = ref( new Map() );
let time = 0;
export default
{
install(app, defFile)
{
...
app.provide( "runs", readonly(
{ ref: rRuns,
get: (e) => rRuns.value.get( e ),
locationNames: () => rRuns.value.keys(),
size: () => rRuns.value.size,
} ) );
...
setInterval( () =>
{ time++;
const key = (time * 7) % 10;
console.log(" runs update", key, time);
rRuns.value.set( key.toString(), time )
}, 1000);
console.log(" time Interval start" );
}
}
main.js:
import { createApp } from 'vue'
import App from './App.vue'
import plugin from 'plugin.js';
const app = createApp(App);
app.config.unwrapInjectedRef = true;
app.use(game, 'gamedefs.json');
app.mount('#app');
runs.vue:
<template>
<h1>Runs:</h1>
<p v-if="!runs.size()">< no runs ></p>
<p v-else>runs: {{ runs.size() }}</p>
<button v-for="r of runs.locationNames()" :key="r" #click="display( r )">[{{ r }}]</button>
</template>
<script>
export default {
name: 'Runs',
inject:
{
runs: { from: 'runs' },
},
watch:
{
'runs.ref':
{
handler( v )
{
console.log("runs.ref watch", v );
},
immediate: true,
deep: true,
},
},
}
</script>

Watch $route.params.id does not trigger re-render of Vue component

I have a Post component which displays user posts. The URL/Route to get to a post is like:
http://localhost:8080/123-todays-bike-ride with 123 being the PostID param in the route.
In my Post.vue component I have the following code:
<template>
<div>
<h1>{{Post.PostTitle}}</h1>
<p>{{Post.Content}}</p>
</div>
</template>
<script>
export default {
name: "Post",
watch:{
'$route.params.PostID': function() {
this.getPost(); // does not seem to be triggered?
}
},
computed:
{
Post() {
return this.$store.getters.getPost
}
},
serverPrefetch() {
return this.getPost(); // to do with SSR
},
mounted() {
if (this.Post == null || !this.Post.length) {
this.getPost();
}
},
methods: {
getPost() {
return this.$store.dispatch('loadPost', {PostID: this.$route.params.PostID})
}
}
}
</script>
The problem is that even if I navigate from http://localhost:8080/123-todays-bike-ride to say http://localhost:8080/999-swimming then the URL in the browser address bar changes but the content of the Post view does not change – it remains with the same content as the 123-todays-bike-ride post.
Clearly the watch: { '$route.params.PostID'... bit is not working, but why and how to solve it?
You can try and make it watch deep:
watch: {
"$route.params.PostID": {
handler: function(value) {
console.log(value);
},
deep: true,
immediate: true,
},
},
Also another way of doing this by re-rendering a component without using watch:
by adding :key="$route.params.PostID" to the Post component like:
inside ParentComponent.vue
<template>
<Post :key="$route.params.PostID" />
</template>

vue.js $emit not received by parent when multiple calls in short amount of time

I tried to implement a simple notification system with notification Store inspired by this snippet form Linus Borg : https://jsfiddle.net/Linusborg/wnb6tdg8/
It is working fine when you add one notification at a time, but when you add a second notification before the first one disappears the notificationMessage emit its "close-notification" event but the parent notificationBox component does not execute the "removeNotification" function. removeNotification is called after the emit if you use the click event on the notification though. So there is probably a issue with the timeout but i can't figure out what.
NotificationStore.js
class NotificationStore {
constructor () {
this.state = {
notifications: []
}
}
addNotification (notification) {
this.state.notifications.push(notification)
}
removeNotification (notification) {
console.log('remove from store')
this.state.notifications.splice(this.state.notifications.indexOf(notification), 1)
}
}
export default new NotificationStore()
App.vue
<template>
<div id="app">
<notification-box></notification-box>
<div #click="createNotif">
create new notification
</div>
</div>
</template>
<script>
import notificationMessage from './components/notificationMessage.vue'
import notificationBox from './components/notificationBox.vue'
import NotificationStore from './notificationStore'
export default {
name: 'app',
methods: {
createNotif () {
NotificationStore.addNotification({
name: 'test',
message: 'this is a test notification',
type: 'warning'
})
}
},
components: {
notificationMessage,
notificationBox
}
}
</script>
notificationBox.vue
<template>
<div :class="'notification-box'">
<notification-message v-for="(notification, index) in notifications" :notification="notification" :key="index" v-on:closeNotification="removeNotification"></notification-message>
</div>
</template>
<script>
import notificationMessage from './notificationMessage.vue'
import NotificationStore from '../notificationStore'
export default {
name: 'notificationBox',
data () {
return {
notifications: NotificationStore.state.notifications
}
},
methods: {
removeNotification: function (notification) {
console.log('removeNotification')
NotificationStore.removeNotification(notification)
}
},
components: {
notificationMessage
}
}
</script>
notificationMessage.vue
<template>
<div :class="'notification-message ' + notification.type" #click="triggerClose(notification)">
{{ notification.message }}
</div>
</template>
<script>
export default {
name: 'notificationMessage',
props: {
notification: {
type: Object,
required: true
},
delay: {
type: Number,
required: false,
default () {
return 3000
}
}
},
data () {
return {
notificationTimer: null
}
},
methods: {
triggerClose (notification) {
console.log('triggerClose')
clearTimeout(this.notificationTimer)
this.$emit('closeNotification', notification)
}
},
mounted () {
this.notificationTimer = setTimeout(() => {
console.log('call trigger close ' + this.notification.name)
this.triggerClose(this.notification)
}, this.delay)
}
}
</script>
thanks for the help
my small fiddle from back in the days is still making the rounds I see :D
That fiddle is still using Vue 1. In Vue 2, you have to key your list elements, and you tried to do that.
But a key should be a unique value identifying a data item reliably. You are using the array index, which does not do that - as soon as an element is removed, indices of the following items change.
That's why you see the behaviour you are seeing: Vue can't reliably remove the right element because our keys don't do their job.
So I would suggest to use a package like nanoid to create a really unique id per notification - but a simple counter would probably work as well:
let _id = 0
class NotificationStore {
constructor () {
this.state = {
notifications: []
}
}
addNotification (notification) {
this.state.notifications.push({ ...notification, id: _id++ })
}
removeNotification (notification) {
console.log('remove from store')
this.state.notifications.splice(this.state.notifications.indexOf(notification), 1)
}
}
and in the notification component:
<notification-message
v-for="(notification, index) in notifications"
:notification="notification"
:key="notification.id"
v-on:closeNotification="removeNotification"
></notification-message>

V-model with props & computed properties

I have a checkbox component that tracks whether or not an item has been saved by the user as a favorite. This information is passed in as a prop.
Because we can't/shouldn't mutate props passed in from a parent component, I am using v-model on a computed property.
<template>
<input class="favorite" type="checkbox" v-model="checked">
</template>
<script>
module.exports = {
props: ['favorite'],
computed: {
checked: {
get: function getChecked() {
return this.favorite;
},
set: function setChecked(newVal) {
this.$emit('update:favorite', newVal);
}
}
}
};
</script>
The parent component controls sending requests to the favorites api & updating the state of each entity if/when the request is successful.
<template>
<input-favorite
#update:favorite="toggleFavorite"
:favorite="entity.favorite"
></input-favorite>
</template>
<script>
module.exports = {
methods: {
toggleFavorite: function toggleFavorite(val) {
if (val) {
this.$store.dispatch('postFavorite', { id: this.entity.id, name: this.entity.name });
} else {
this.$store.dispatch('deleteFavorite', this.entity.id);
}
}
}
};
</script>
If the request fails, however, is it possible to prevent the checkbox from getting checked in the first place? Both this.favorite and this.checked stay in sync, but the state of the checkbox does not.
Because the data & props stay correct, I'm also having trouble figuring out how I could trigger a re-render of the checkbox to get it back to the correct state.
I suspect the problem is that favorite never changes, so Vue doesn't see a need to update. You should update it to true upon receiving the checked value (so state is consistent) and then update it again to false when the request fails.
Vue.component('inputFavorite', {
template: '#input-favorite',
props: ['favorite'],
computed: {
checked: {
get: function getChecked() {
return this.favorite;
},
set: function setChecked(newVal) {
this.$emit('update:favorite', newVal);
}
}
}
});
new Vue({
el: '#app',
data: {
entity: {
favorite: false
}
},
methods: {
toggleFavorite: function toggleFavorite(val) {
if (val) {
console.log("Post");
this.entity.favorite = true;
// Mock up a failure
setTimeout(() => {
console.log("Failed");
this.entity.favorite = false;
}, 250);
} else {
console.log("Delete");
}
}
}
});
<script src="//cdnjs.cloudflare.com/ajax/libs/vue/2.3.4/vue.min.js"></script>
<template id="input-favorite">
<input class="favorite" type="checkbox" v-model="checked">
</template>
<div id="app">
<input-favorite #update:favorite="toggleFavorite" :favorite="entity.favorite"></input-favorite>
</div>
The way you have set this up lends itself to the recently-reintroduced .sync modifier, which would simplify your HTML a bit:
<input-favorite :favorite.sync="entity.favorite"></input-favorite>
Then you do away with toggleFavorite and instead add a watch:
watch: {
'entity.favorite': function (newValue) {
console.log("Updated", newValue);
if (newValue) {
setTimeout(() => {
console.log("Failed");
this.entity.favorite = false;
}, 250);
}
}
}