How can I access a layout- or page-function directly in a component? Is there a special variable like $root or $parent?
I found a way to do this, but it seems dirty. I saw the component structure using Vue DevTool, and I found the layout is the root's child, so I called the layout's function like this:
this.$root.$children[2].getMap()
Is there a cleaner way?
You could use Vue's provide/inject feature for this. For instance, a page/layout could provide the getMap():
<template>
<MapButton />
</template>
<script>
export default {
name: 'MapPage',
provide() {
return {
getMap() {
return { /* map */ }
}
}
}
}
</script>
...and then any child component on that page/layout could inject the method where needed:
<template>
<button #click="getMap">Get map</button>
</template>
<script>
export default {
name: 'MapButton',
inject: {
getMap: {
default() {
return () => {
console.log('no map')
}
}
}
},
mounted() {
console.log('map', this.getMap())
}
}
</script>
Related
I have the problem, that a component doesn't recognize the change of a property.
The component is nested about 5 levels deep. Every component above the faulty one does update with the same mechanics and flawlessly.
I invested some time to get to the problem, but I can't find it.
The flow is:
Dashboard (change value and pass as prop)
TicketPreview (Usage and
pass prop)
CommentSection (Pass prop)
CommentList (FAULTY / Usage of prop)
Everything down to the commentSection is being updated as expected, but the commentList doesn't get the update notification (beforeUpdate doesn't get triggered).
Since I tested quite a few things I will only post the essential code from commentSection (parent) and commenList (child)
DISCLAIMER: This is a prototype code without backend, therefore typical API-Requests are solved with the localStorage of the users browser.
commentSection
<template>
<div id="comment-section">
<p>{{selectedTicket.title}}</p>
<comment-form :selectedTicket="selectedTicket" />
<comment-list :selectedTicket="selectedTicket" />
</div>
</template>
<script>
import CommentForm from "#/components/comment-section/CommentForm";
import CommentList from "#/components/comment-section/CommentList";
export default {
name: "CommentSection",
components: {
CommentForm,
CommentList,
},
props: {
selectedTicket: Object,
},
beforeUpdate() {
console.log("Comment Section");
console.log(this.selectedTicket);
},
updated() {
console.log("Comment Section is updated");
}
}
</script>
CommentList
<template>
<div id="comment-list">
<comment-item
v-for="comment in comments"
:key="comment.id"
:comment="comment"
/>
</div>
</template>
<script>
import CommentItem from "#/components/comment-section/CommentItem";
export default {
name: "CommentList",
components: {
CommentItem,
},
data() {
return {
comments: Array,
}
},
props: {
selectedTicket: Object,
},
methods: {
getComments() {
let comments = JSON.parse(window.localStorage.getItem("comments"));
let filteredComments = [];
for(let i = 0; i < comments.length; i++){
if (comments[i].ticketId === this.selectedTicket.id){
filteredComments.push(comments[i]);
}
}
this.comments = filteredComments;
}
},
beforeUpdate() {
console.log("CommentList");
console.log(this.selectedTicket);
this.getComments();
},
mounted() {
this.$root.$on("updateComments", () => {
this.getComments();
});
console.log("CL Mounted");
},
}
</script>
The beforeUpdate() and updated() hooks from the commentList component are not being fired.
I guess I could work around it with an event passing the data, but for the sake of understanding, let's pretend it's not a viable option right now.
It would be better to use a watcher, this will be more simple.
Instead of method to set comments by filtering you can use computed property which is reactive and no need to watch for props updates.
CommentSection
<template>
<div id="comment-section">
<p>{{ selectedTicket.title }}</p>
<comment-form :selectedTicket="selectedTicket" />
<comment-list :selectedTicket="selectedTicket" />
</div>
</template>
<script>
import CommentForm from "#/components/comment-section/CommentForm";
import CommentList from "#/components/comment-section/CommentList";
export default {
name: "CommentSection",
components: {
CommentForm,
CommentList
},
props: {
selectedTicket: Object
},
methods: {
updateTicket() {
console.log("Comment section is updated");
console.log(this.selectedTicket);
}
},
watch: {
selectedTicket: {
immediate: true,
handler: "updateTicket"
}
}
};
</script>
CommentList
<template>
<div id="comment-list">
<comment-item
v-for="comment in comments"
:key="comment.id"
:comment="comment"
/>
</div>
</template>
<script>
import CommentItem from "#/components/comment-section/CommentItem";
export default {
name: "CommentList",
components: {
CommentItem
},
props: {
selectedTicket: Object
},
computed: {
comments() {
let comments = JSON.parse(window.localStorage.getItem("comments"));
let filteredComments = [];
for (let comment of comments) {
if (comment.ticketId == this.selectedTicket.id) {
filteredComments.push(comment);
}
}
// // using es6 Array.filter()
// let filteredComments = comments.filter(
// (comment) => comment.ticketId == this.selectedTicket.id
// );
return filteredComments;
}
}
};
</script>
I found the problem: Since commentList is only a wrapper that doesn't use any of the values from the prop, the hooks for beforeUpdate and updated are never triggered. The Vue Instance Chart is misleading in that regard. The diagram shows it like beforeUpdate would ALWAYS fire, when the data changed (then re-render, then updated), but beforeUpdate only fires if the Component and Parent has to be re-rendered.
The Object updates as expected, it just never triggered a re-render on the child component because the wrapper has not been re-rendered.
Is it possible to extend child component function at runtime in vue? I want to limit/stop child component function call based on parent scope logic (I want to avoid passing props in this specific case).
Overriding a component method is not a runtime solution/I can't have access to parent scope.
What I have tried and it does not working:
// Foo.vue
<template>
<button #click="func">Click me</button>
</template>
export default {
methods: {
func() {
console.log('some xhr')
}
}
}
// Bar.vue
<template>
<Foo ref="foo"/>
</template>
export default {
components: {Foo}
mounted() {
this.$nextTick(() => {
this.$refs.foo.func = function() {
console.log('some conditional logic')
this.$refs.foo.func()
}
})
}
}
For this usecase a better implementation would be defining the function in the parent itself and passing it through props. Since props are by default reactive you can easily control it from parent.
// Foo.vue
<template>
<button #click="clickFunction.handler">Click me</button>
</template>
export default {
name: 'Foo',
props: {
clickFunction: {
type: Object,
required: true
}
}
}
// Bar.vue
<template>
<Foo :clickFunction="propObject"/>
</template>
export default {
components: {Foo},
data() {
return {
propObject: {
handler: null;
}
};
}
mounted() {
this.$nextTick(() => {
if(some condition) {
this.propObject.handler = this.func();
} else this.propObject.handler = null;
})
},
methods: {
func() {
console.log('some xhr')
}
}
}
From what I managed to realize:
the solution in the code posted in the question really replaces the func() method in the child component. It's just that Vue has already attached the old method to the html element. Replacing it at the source will have no impact.
I was looking for a way to re-attach the eventListeners to html component. Re-rendering using an index key would not help because it will re-render the component with its original definition. You can hide the item in question for a split second, and when it appears you will receive an updated eventListener. However, this involves an intervention in the logic of the child component (which I avoid).
The solution is the $forceUpdate() method.
Thus, my code becomes the following:
// Foo.vue
<template>
<button #click="func">Click me</button>
</template>
export default {
methods: {
func() {
console.log('some xhr')
}
}
}
// Bar.vue
<template>
<Foo ref="foo"/>
</template>
export default {
components: {Foo}
mounted() {
this.$nextTick(() => {
let original = this.$refs.foo.func; // preserve original function
this.$refs.foo.func = function() {
console.log('some conditional logic')
original()
}
this.$refs.btn.$forceUpdate(); // will re-evaluate visual logic of child component
})
}
}
This is my approach of calling a method after v-for has done looping. It works perfectly fine but i still want to know if there's a better approach than this or if vue is offering something like v-for callback.
Parent Component
<template>
<loader-component v-show="show_loader" />
<list-component #onRendered="hideLoader"/>
</template>
<script>
export default {
components: {
loaderComponent,
listComponent
},
data() {
return {
show_loader: true
}
}
methods: {
hideLoader() {
this.show_loader = false;
}
}
}
</script>
List Component
<template>
<item-component v-for="(item, key) in items" :isRendered="isRendered(key)" />
</template>
<script>
export default {
prop: ['items'],
methods: {
isRendered(key) {
let total_items = this.items.length - 1;
if (key === total_items) {
this.$emit('onRendered');
}
}
}
}
</script>
I believe there is a misunderstanding in how exactly Vue updates its reactive properties.
I made a little demo for you hoping it clears it up.
https://codesandbox.io/s/prod-star-pzjhc
I would also recommend some reading from the Vue docs computed properties
How do I call the method from another component in this scenario? I would like to load additional pice of data from the API once the button is clicked in the component 1 to the component 2.
Thanks
Here are my two components in the seperate files:
compbutton.vue
<template>
<div>
<a href v-on:click="buttonClicked">Change Name</a>
</div>
</template>
<script>
export default {
name: 'compbutton',
methods: {
buttonClicked: function () {
//call changeName here
}
}
}
</script>
compname.vue
<template>
<div>{{name}}</div>
</template>
<script>
export default {
name: 'compname',
data: function () {
return {
name: 'John'
}
},
methods: {
changeName: function () {
this.name = 'Ben'
}
}
}
</script>
You can name the component and then $ref to the method from another componenent.
compbutton.vue
<template>
<div>
<a href v-on:click="buttonClicked">Change Name</a>
</div>
</template>
<script>
export default {
name: "compbutton",
methods: {
buttonClicked: function() {
//call changeName here
this.$root.$refs.compname_component.changeName();
}
}
};
</script>
compname.vue
<template>
<div>{{name}}</div>
</template>
<script>
export default {
name: "compname",
data: function() {
return {
name: "John"
};
},
methods: {
changeName: function() {
this.name = "Ben";
}
},
created() {
// set componenent name
this.$root.$refs.compname_component = this;
}
};
</script>
Alternative answer: you can pass the function you want the child to invoke as a prop from the parent component. Using your example:
compbutton.vue
<template>
<div>
<a href v-on:click="buttonClicked">Change Name</a>
</div>
</template>
<script>
export default {
name: 'compbutton',
props: {
clickHandler: {
type: Function,
default() {
return function () {};
}
}
},
methods: {
buttonClicked: function () {
this.clickHandler(); // invoke func passed via prop
}
}
}
</script>
compname.vue
<template>
<div>{{name}}</div>
<compbutton :click-handler="changeName"></compbutton>
</template>
<script>
export default {
name: 'compname',
data: function () {
return {
name: 'John'
}
},
methods: {
changeName: function () {
this.name = 'Ben'
}
}
}
</script>
Note, in your example, it doesn't appear where you want the 'compbutton' component to be rendered, so in the template for compname.vue, thats been added as well.
You can use a service as a go-between. Usually, services are used to share data but in javascript functions can be treated like data also.
The service code is trivial, just add a stub for the function changeName
changeName.service.js
export default {
changeName: function () {}
}
To have services injected into the components, you need to include vue-injector in the project.
npm install --save vue-inject
or
yarn add vue-inject
and have a register of services,
injector-register.js
import injector from 'vue-inject';
import ChangeNameService from '#/services/changeName.service'
injector.service('changeNameService', function () {
return ChangeNameService
});
then in main.js (or main file may be called index.js), a section to initialize the injector.
import injector from 'vue-inject';
require('#/services/injector-register');
Vue.use(injector);
Finally, add the service to the component dependencies array, and use the service
compname.vue
<script>
export default {
dependencies : ['changeNameService'],
created() {
// Set the service stub function to point to this one
this.changeNameService.changeName = this.changeName;
},
...
compbutton.vue
<script>
export default {
dependencies : ['changeNameService'],
name: 'compbutton',
methods: {
buttonClicked: function () {
this.changeNameService.changeName();
}
}
...
Add a # to the button href to stop page reloads
Change Name
See the whole thing in CodeSandbox
Vue.js version: 2.4.2
Below component always print this.$listeners as undefined.
module.exports = {
template: `<h1>My Component</h1>`,
mounted() {
alert(this.$listeners);
}
}
I register the component and put it inside a parent component.
Can someone tell me why?
You have to understand what $listeners are.
this.$listeners will be populated once there are components that listen to events that your components is emitting.
let's assume 2 components:
child.vue - emits an event each time something is written to input field.
<template>
<input #input="emitEvent">
</input>
</template>
<script>
export default {
methods: {
emitEvent() {
this.$emit('important-event')
console.log(this.$listeners)
}
}
}
</script>
parent.vue - listen to the events from child component.
<template>
<div class="foo">
<child #important-event="doSomething"></child>
</div>
</template>
<script>
import child from './child.vue'
export default {
data() {
return {
newcomment: {
post_id: 'this is default value'
}
}
},
components: { child },
methods: {
doSomething() {
// do something
}
}
}
</script>
With this setup, when you type something to the input field, this object should be written to the console:
{
`important-event`: function () { // some native vue.js code}
}
I added the following alias to my webpack.config.js file and this resolved the issue for me:-
resolve: {
alias: {
'vue$': path.resolve(__dirname, 'node_modules/vue/dist/vue.js')
}
},