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>
Related
I have one component in which I use axios to get data from API, it works and I can use this data in this component, but when I try to provide this data to another component, I dont get any data there.
Here is part of my code:
data() {
return {
theme: [],
};
},
provide() {
return {
theme: this.theme
}
},
methods: {
getTheme() {
axios
.get(here is my api url)
.then((response) => (this.theme = response.data.data));
},
},
mounted() {
this.getTheme();
},
and this is the second component:
<template>
<div class="project-wrapper">
<project-card
v-for="course in theme.courses"
:name="course.name"
:key="course.id"
></project-card>
</div>
</template>
<script>
import ProjectCard from "../components/ProjectCard.vue";
export default {
inject: ["theme"],
components: {
ProjectCard,
}
};
</script>
What is wrong with my code?
Second option in the link may help you
provide() {
return {
$theme: () => this.theme,
}
},
and
inject: ["$theme"],
computed: {
computedProperty() {
return this.$theme()
}
}
and
v-for="course in computedProperty.courses"
When you set provide to 'handle' theme it adds reactivity to the value of theme - i.e the empty array ([]).
If you modify the elements in this array, it will remain reactive - however if you replace the array then the reactivity is broken.
Instead of overwriting theme in the axios call, try adding the resulting data to it. For example:
getTheme() {
axios
.get(here is my api url)
.then((response) => (this.theme.push(...response.data.data));
}
You are passing theme to the child component as injected property.
See Vue.js Docs:
The provide and inject bindings are NOT reactive. This is intentional.
However, if you pass down an observed object, properties on that
object do remain reactive.
As inject bindings are not reactive, the changed value of theme will not be visible from inside of the child component (it will stay the same as if no axios call happened).
Solution 1
Pass the value to the child component as an observed object. It means that in in your getTheme() method you will not rewrite the whole property value (this.theme = ...) but only write into the object which is already stored in the property (this.theme.themeData = ...).
data() {
return {
theme: { },
};
},
provide() {
return {
theme: this.theme
}
},
methods: {
getTheme() {
axios
.get(here is my api url)
.then((response) => (this.theme.themeData = response.data.data));
},
},
mounted() {
this.getTheme();
}
Solution 2
Alternatively you can pass the value to the child component using classical props which are always reactive.
I have an action in Vuex actions which commit a mutation that it take a payload from the component, that is a number of the index for returning an object, it works fine on Vuex js file meaning that shows the selected item on the console, as I said it gets index from the payload,
but on the component, it gives me Promise <Pending>, why that's happening? for now, I do not use any API for my Nuxt/Vue app, but I will, and for now, I just want to know why this is happening and what is the best solution for solving this
Here my Vuex codes:
export const state = () => ({
articles: [
{
uid: 0,
img: 'https://raw.githubusercontent.com/muhammederdem/mini-player/master/img/1.jpg',
link: '/articles/1',
},
{
uid: 1,
img: 'https://raw.githubusercontent.com/muhammederdem/mini-player/master/img/2.jpg',
link: '/articles/2',
},
],
})
export const getters = {
getArticles(state) {
return state.articles
},
}
export const mutations = {
getSpeceficArticle(state, payload) {
return state.articles[payload]
},
}
export const actions = {
getSpeceficArticle({ commit }, payload) {
commit('getSpeceficArticle', payload)
},
}
and here my component codes:
<template>
<div class="article">
{{ getSpeceficArticle() }}
<div class="article__banner">
<img src="" alt="" />
</div>
<div class="article__text">
<p></p>
</div>
</div>
</template>
<script>
export default {
name: 'HomeArticlesArticle',
data() {
return {
item: '',
}
},
// computed: {},
methods: {
async getSpeceficArticle() {
return await this.$store.dispatch('articles/getSpeceficArticle', 0)
},
},
}
</script>
actions are used to update the state they are like mutations but the main difference between them is that actions can include some asynchronous tasks, if you want to get a specific article at given index you should use a getter named getArticleByIndex :
export const getters = {
getArticles(state) {
return state.articles
},
getArticleByIndex:: (state) => (index) => {
return state.articles[index]
}
}
then define a computed property called articleByIndex :
<script>
export default {
name: 'HomeArticlesArticle',
data() {
return {
item: '',
}
},
computed: {
articleByIndex(){
return this.$store.getters.articles.getArticleByIndex(0)
}
},
methods: {
},
}
</script>
#Mohammad if you find yourself using a lot of getters/actions etc from Vuex and they're starting to get a little wordy, you can bring in mapGetters from Vuex and rename your calls to something a little more convenient. So your script would become,
<script>
import { mapGetters } from 'vuex'
export default {
name: 'HomeArticlesArticle',
data() {
return {
item: '',
}
},
computed: {
articleByIndex(){
return this.getArticleByIndex(0)
}
},
methods: {
...mapGetters({
getArticleByIndex: 'articles/getArticleByIndex',
})
},
}
</script>
You can add ...mapGetters, ...mapActions to your computed section also.
since there is no web service call in vuex action, try to remove async and await keywords from the component.
Later when you add a webservice call than you can wrap action body in new Promise with resolve and reject and then you can use async and await in component. let me know if this works for you.
I need a single file component to load its template via AJAX.
I search a while for a solution and found some hints about dynamic components.
I crafted a combination of a parent component which imports a child component and renders the child with a dynamic template.
Child component is this:
<template>
<div>placeholder</div>
</template>
<script>
import SomeOtherComponent from './some-other-component.vue';
export default {
name: 'child-component',
components: {
'some-other-component': SomeOtherComponent,
},
};
</script>
Parent component is this
<template>
<component v-if='componentTemplate' :is="dynamicComponent && {template: componentTemplate}"></component>
</template>
<script>
import Axios from 'axios';
import ChildComponent from './child-component.vue';
export default {
name: 'parent-component',
components: {
'child-component': ChildComponent,
},
data() {
return {
dynamicComponent: 'child-component',
componentTemplate: null,
};
},
created() {
const self = this;
this.fetchTemplate().done((htmlCode) => {
self.componentTemplate = htmlCode;
}).fail((error) => {
self.componentTemplate = '<div>error</div>';
});
},
methods: {
fetchTemplate() {
const formLoaded = $.Deferred();
const url = '/get-dynamic-template';
Axios.get(url).then((response) => {
formLoaded.resolve(response.data);
}).catch((error) => {
formLoaded.reject(error);
}).then(() => {
formLoaded.reject();
});
return formLoaded;
},
},
};
</script>
The dynamic template code fetched is this:
<div>
<h1>My dynamic template</h1>
<some-other-component></some-other-component>
</div>
In general the component gets its template as expected and binds to it.
But when there are other components used in this dynamic template (some-other-component) they are not recognized, even if they are correctly registered inside the child component and of course correctly named as 'some-other-component'.
I get this error: [Vue warn]: Unknown custom element: some-other-component - did you register the component correctly? For recursive components, make sure to provide the "name" option.
Do I miss something or is it some kind of issue/bug?
I answer my question myself, because I found an alternative solution after reading a little bit further here https://forum.vuejs.org/t/load-html-code-that-uses-some-vue-js-code-in-it-via-ajax-request/25006/3.
The problem in my code seems to be this logical expression :is="dynamicComponent && {template: componentTemplate}". I found this approach somewhere in the internet.
The original poster propably assumed that this causes the component "dynamicComponent" to be merged with {template: componentTemplate} which should override the template option only, leaving other component options as defined in the imported child-component.vue.
But it seems not to work as expected since && is a boolean operator and not a "object merge" operator. Please somebody prove me wrong, I am not a JavaScript expert after all.
Anyway the following approach works fine:
<template>
<component v-if='componentTemplate' :is="childComponent"></component>
</template>
<script>
import Axios from 'axios';
import SomeOtherComponent from "./some-other-component.vue";
export default {
name: 'parent-component',
components: {
'some-other-component': SomeOtherComponent,
},
data() {
return {
componentTemplate: null,
};
},
computed: {
childComponent() {
return {
template: this.componentTemplate,
components: this.$options.components,
};
},
},
created() {
const self = this;
this.fetchTemplate().done((htmlCode) => {
self.componentTemplate = htmlCode;
}).fail((error) => {
self.componentTemplate = '<div>error</div>';
});
},
methods: {
fetchTemplate() {
const formLoaded = $.Deferred();
const url = '/get-dynamic-template';
Axios.get(url).then((response) => {
formLoaded.resolve(response.data);
}).catch((error) => {
formLoaded.reject(error);
}).then(() => {
formLoaded.reject();
});
return formLoaded;
},
},
};
</script>
I have a child component that's basically a search box. When the user types something and presses enter, an event is fired that goes to the parent with the search topic:
export default {
name: "SearchBar",
methods: {
searchRequested(event) {
const topic = event.target.value;
this.$emit('searchRequested', topic);
}
}
};
The parent receives the event and updates a prop connected to other of its children (an image gallery):
<template>
<div id="app">
<SearchBar #searchRequested="onSearchRequested($event)" />
<Images :topic="topic" />
</div>
</template>
<script>
import SearchBar from './components/SearchBar.vue'
import Images from './components/Images.vue'
export default {
name: 'app',
components: {
SearchBar,
Images
},
data() {
return {
topic: ''
};
},
methods: {
onSearchRequested(topic) {
this.topic = topic;
}
}
}
</script>
So far, so good. But now I want the child component load itself with images related to the searched topic whenever the user performs a new search. For that, the child component Images must be aware of a change on its property topic, so I created a computed one:
import { ImagesService } from '../services/images.service.js';
export default {
data() {
return {
topic_: ''
};
},
methods: {
updateImages() {
const images = new ImagesService();
images.getImages(this.topic_).then(rawImages => console.log(rawImages));
}
},
computed: {
topic: {
get: function() {
return this.topic_;
},
set: function(topic) {
this.topic_ = topic;
this.updateImages();
}
}
}
};
But unfortunately, the setter never gets called. I have to say I'm new in Vue, so probably I'm doing something wrong. Any help will be appreciated.
You don't need to create computed in the main component. Images component is already aware of the changes in the topic prop.
You need to watch the changes of topic and do an async operation in 'Images.vue'. It's possible with Vue's watchers.
Vue docs watchers
'./components/Images.vue'
<template>...</template>
<script>
export defult {
props: ['topic'],
data(){
return {
images: []
}
},
watch: {
topic(newVal){
// do async opreation and update data.
// ImageSerice.get(newVal)
// .then(images => this.images = images)
}
}
}
</script>
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>