Can't modify top level data of slot object in Vue - javascript

If you run this demo and click "modify in child", the text gets updated. But if you click "modify top level through slot", then it doesn't get updated, and after clicking it clicking the other button no longer works.
How can I update a top-level property of the slot? For example a boolean or string. Doing it directly in the child works, but I can't do it through the slot.
If the child data contains an object, I can modify a sub property of that data object through the slot (see the original version of this question before the edits for a demo of that), but I can't modify a top level property.
const Child = {
template: `<div>
{{ object }}
<slot name="named" v-bind="object">
</slot>
<button #click="click">child</button>
</div>`,
data() {
return {
object: {
string: "initial"
}
}
},
methods: {
click() {
this.object.string = "modify in child"
}
}
}
new Vue({
components: {
Child,
},
template: `
<div class="page1">
<Child>
<template v-slot:named="slot">
<button #click="click(slot)">modify top level through slot</button>
</template>
</Child>
</div>`,
methods: {
click(slot) {
slot.string = "updated top level through slot"
}
}
}).$mount('#app')
<div id="app"></div>
<script src="https://cdn.jsdelivr.net/npm/vue#2.6.11/dist/vue.min.js"></script>

As to why you are seeing this behavior where you can't modify the root object, but can modify properties underneath it, the exposed slot variable is a shallow clone, so the top level reference is not the same as the object reference in the child component. I've added a console.log and wrapped .string in an object to show it. Click child and then modify buttons.
So it looks like you are trying to expose the state of the child component to the parent component, so you can reach in from the parent and mutate the state of the child. This is generally not the way you are supposed to use Vue. The idea is that state should be moved higher up in the tree, and deterministic props are propogated down through your tree of components.
Directly referencing and mutating state on child components is an antipattern. This is to encourage designing components that have deterministic behavior and to maintain decoupling of the components (to keep them standalone and reusable). There's also some performance benefits.
This guy explains it well: https://stackoverflow.com/a/31756470/120242
Vue and React are based on similar concepts.
const Child = {
template: `<div>
{{ object }}
<slot name="named" v-bind="object">
</slot>
<button #click="click">child</button>
</div>`,
data() {
return {
object: {
string: {x: "initial"}
}
}
},
methods: {
click() {
window.ChildObjectReference = this.object;
this.object.string.x = "modify in child"
}
}
}
new Vue({
components: {
Child,
},
template: `
<div class="page1">
<Child>
<template v-slot:named="slot">
<button #click="click(slot)">modify top level through slot</button>
</template>
</Child>
</div>`,
methods: {
click(slot) {
console.log( `slot: `,slot, `\nobject = `, window.ChildObjectReference,
`\nslot !== ref as object: `, slot === window.ChildObjectReference,
`\nslot.string === ref object.string: `, slot.string === window.ChildObjectReference.string)
}
}
}).$mount('#app')
<div id="app"></div>
<script src="https://cdn.jsdelivr.net/npm/vue#2.6.11/dist/vue.min.js"></script>
Displaying interaction between slot and parent component:
const Child = {
template: `<div>
{{ object }}
<slot name="named" v-bind="object">
</slot>
<button #click="click">child</button>
</div>`,
data() {
return {
object: {
string: "initial"
}
}
},
methods: {
click() {
this.object.string = "modify in child"
}
}
}
new Vue({
data: {
topLevelObject: { property: "top level initial" }
},
components: {
Child,
},
template: `
<div class="page1">
<Child>
<template v-slot:named="slot">
<div>this is v-bind:'object' on slot 'named' put into variable slot: {{ slot }}</div>
<button #click="click(slot)">modify top level through slot</button>
</template>
</Child>
Top Level state: {{ topLevelObject }}
</div>`,
methods: {
click(slot) {
this.topLevelObject.property = "slot.string pushed to top level: " + slot.string
}
}
}).$mount('#app')
<div id="app"></div>
<script src="https://cdn.jsdelivr.net/npm/vue#2.6.11/dist/vue.min.js"></script>

Related

Please note that slots are not reactive

Why is slot said to be no reactive?
https://v2.vuejs.org/v2/api/index.html#vm-slots
Following is the example, when i click component's button, the headerValue will add, if the slot is no reactive, then the component childern will not render, but actually it's opposite
<!-- component A -->
<template>
<div>
<header-slot>
<template slot="header">
<div>{{ headerValue }}</div>
</template>
</header-slot>
<button #click="changeProp">change</button>
</div>
</template>
<script>
import HeaderSlot from '#/views/headerSlot.vue'
export default {
components: {
HeaderSlot
},
data() {
return {
headerValue: 1
}
},
methods: {
changeProp() {
this.headerValue += 1
}
}
}
</script>
<template>
<!-- component children -->
<h1>
inner-text
<slot name="header" />
</h1>
</template>
The example you're referring to is using this snippet of code
Vue.component('blog-post', {
render: function (createElement) {
var header = this.$slots.header // 👈🏻 referring to that slot
var body = this.$slots.default
var footer = this.$slots.footer
return createElement('div', [
createElement('header', header),
createElement('main', body),
createElement('footer', footer)
])
}
})
aka using this.$slots.header in a render function is probably not reactive.
Of course, if you have a slot in place and you have some updates on a data or computed state, it will be reactive and update itself but here, the given point is about the vm.$slots (which is read only btw).

Vue.js - no access to this.$parent when child component is inside <transition>

What I want: I have two components, the parent component (Wall.vue) and the child component (PostItem.vue). Every PostItem has a delete button. On click, a request to my API is sent and the item gets deleted from the database. Then I want to call the getPosts function of the parent component to get all the posts again (this time without the deleted post).
The Problem: Inside the child component, I have no access to the this.$parent Object (or more specific, it's just empty and doesn't contain the functions), so I can't call the getPosts-Function. When I remove the <transition-group> in the parent component that surrounds also the child-component, everything works fine.
What is the problem here?
Parent-Component (Wall.vue)
template-portion:
<template>
<div class="Wall view">
<transition-group name="wallstate">
<template v-else-if="messages">
<PostItem
v-for="(message, index) in messages"
:key="index"
:message="message"
:index="index"
class="PostItem"
/>
</template>
<h1 v-else>
Could not load messages. Please try later.
</h1>
</transition-group>
</div>
</template>
script-portion:
<script>
import { mapGetters } from 'vuex';
import { postsAPI } from '../services/posts.service.js';
import PostItem from '../components/PostItem.vue';
export default {
components: {
PostItem,
},
data() {
return {
messages: null,
};
},
methods: {
getPosts() {
///////Do stuff
}
}
};
</script>
Child-Component (PostItem.vue)
template-portion
<template>
<div class="PostItem__message frosted">
<p class="PostItem__messageContent">{{ message.content }}</p>
<p>
by: <strong>{{ message.user.username }}</strong>
</p>
<a
#click="deletePost"
:data-id="message._id"
v-if="message.user._id === user.id"
>
Delete
</a>
</div>
</template>
script-portion:
<script>
import { postsAPI } from '../services/posts.service.js';
import { mapGetters } from 'vuex';
export default {
name: 'PostItem',
props: {
message: {
type: Object,
required: true,
},
index: {
type: Number,
required: true,
},
},
computed: {
...mapGetters({
user: 'auth/user',
}),
},
methods: {
deletePost(e) {
const id = e.target.dataset.id;
postsAPI.removeOne(id).then((res) => {
this.$parent.getPosts(); <-------- PROBLEM HERE
});
},
},
};
</script>
It's generally considered a bad practice to use this.$parent (it couples the components and reduces encapsulation / code clarity.) The child component should emit an event when it wants to send information to an ancestor component.
Remove the direct access and $emit an event called 'deleted':
deletePost(e) {
const id = e.target.dataset.id;
postsAPI.removeOne(id).then((res) => {
this.$emit('deleted'); // Emitting the event
});
},
The parent should listen for that deleted event and run an event handler:
<PostItem
v-for="(message, index) in messages"
:key="index"
:message="message"
:index="index"
class="PostItem"
#deleted="getPosts"
/>
The parent will call the getPosts method when triggered by the #deleted event listener.
inside the methods part, instead of :
methods: {
deletePost(e) {
const id = e.target.dataset.id;
postsAPI.removeOne(id).then((res) => {
this.$parent.getPosts();
});
},
},
you may try this:
methods: {
deletePost(e) {
const id = e.target.dataset.id;
let self=this;
postsAPI.removeOne(id).then((res) => {
self.$parent.getPosts();
});
}
Because of the scope chain, 'this' inside .then() does not point to the same variable environment as variable 'self' does. So perhaps it's the reason it fails to work.

Calling a parent function from a child component raises an error in Vue.js

I have a navigation drawer child component inside my parent component.
MainComponent.vue
<template>
<v-app>
<div>
<NavigationComponent></NavigationComponent>
</div>
</v-app>
</template>
Now, in the child component (Navigation drawer), I tried to call a function from the MainComponent by doing:
this.$parent._appendUser(arr);
I have a similar parent-child component that calls a function from parent to child, but I don't know why this one gives me an error saying:
TypeError: this.$parent._appendUseris not a function
Parent Component - MainComponent.vue
<template>
<v-app>
<div class="container" style="padding: 0 !important">
<div class="row">
</div>
<AddInstrumentDrawer :add_instrument_drawer_watcher="add_instrument_drawer_watcher"></AddInstrumentDrawer>
</div>
</v-app>
</template>
<script>
import AddInstrumentDrawer from './shared/AddInstrumentDrawer.vue'
export default {
name: 'user-profile',
components: { AddInstrumentDrawer },
data: () => ({
profile_image_drawer_watcher: 1,
add_instrument_drawer_watcher: 1,
}),
methods: {
appendinstrument(arr) {
alert(arr);
/*for (let i = 0; i < arr.length; i++) {
this.profile_data.instruments.push(arr[i]);
} */
},
}
}
</script>
Child Component
<template>
<v-app>
<div>
<v-navigation-drawer v-model="add_drawer" fixed temporary right width="600"
disable-resize-watcher disable-route-watcher
style="z-index: 101 !important">
<div class="drawer-footer">
<div class="drawer-footer-content-2">
<button type="button" class="btn mx-1" #click="test()">Save changes</button>
</div>
</div>
</v-navigation-drawer>
</div>
</v-app>
</template>
<script>
export default {
name: 'add-instrument-drawer',
components: { },
props: ['add_instrument_drawer_watcher'],
data: () => ({
add_drawer: false,
}),
watch: {
add_instrument_drawer_watcher: function(n, o) {
this.add_drawer = !this.add_drawer;
},
},
methods: {
test() {
this.$parent.appendinstrument('test');
},
},
}
</script>
You shouldn't call parent method using this.$parent.someMethod() but you have to emit a custom event from child component to the parent one which has the parent method as handler :
<AddInstrumentDrawer
#append-instrument="appendinstrument"
:add_instrument_drawer_watcher="add_instrument_drawer_watcher"></AddInstrumentDrawer>
in child component :
methods: {
test() {
this.$emit('append-instrument','test');
},
},
It's a bad practice to call parent methods from child component. Components should not be aware of parent implementation. I suggest you emit an event in your navigation drawer and handle the event in your main component
//Navigation.vue
$emit('append-user', arr)
//Main.vue
<Navigation #append-user="appendUser" />
This way you can place your navigation component within any component.
One more thing about $parent is that it does not reference the component that includes the target but the immediate component in the component tree where component is placed (think about DOM traversal)
//Main.vue
<ComA>
<ComB />
</ComA>
Inside ComB $parent will reference ComA
and not Main component

How to access slot props in the created hook?

I'm trying to access properties I'm passing on to my slot. But my slotProps are undefined.
As I'm still new to Vue and I've read their docs I still can't seem to figure out why I can't access the props data.
Problem
I'm trying to access the slotProps in my child components created, but it's undefined
emphasized text
<template>
<div>
<slot :data="data" :loading="loading"></slot>
</div>
</template>
Child
<template v-slot:default="slotProps">
<div >
</div>
</template>
<script>
export default {
name: "child"
created: function() {
console.log("slotProps", slotProps);
}
};
</script>
You can use this object to follow with your child property
demo: https://stackblitz.com/edit/vue-kkhwzc?file=src%2Fcomponents%2FHelloWorld.vue
Updated code Child
<template v-slot:default="slotProps">
<div >
</div>
</template>
<script>
export default {
name: "child"
created: function() {
console.log("slotProps", this.slotProps);
}
};
</script>
You do not need the created() life cycle hook to achieve what you want. There are few things to clear up:
What you are using is actually called scoped slots. They are useful because, unlike when using the default and named slots, the parent component can not access the data of its child component(s).
What you call a Child is actually the parent component.
Child.vue component should be something like this:
<template>
<div>
<main>
<slot :data="data1" :loading="loading1" />
</main>
</div>
</template>
<script>
export default {
name: 'Page',
data () {
return {
data1: 'foo',
loading1: 'bar'
}
}
}
</script>
In a Parent.vue component, you can access the data of the above component as follows:
<template>
<child>
<template v-slot="slotProps">
{{ slotProps.data }},
{{ slotProps.loading }}
</template>
</child>
</template>
<script>
import Child from '#/components/Child.vue'
export default {
components: { Child }
}
</script>
Or you can also destruct the objects on the fly as follows:
<template>
<child>
<template v-slot="{data, loading }">
{{ data }},
{{ loading }}
</template>
</child>
</template>
<script>
import Child from '#/components/Child.vue'
export default {
components: { Child }
}
</script>
This is the clean way to access data of a child component from the parent using scoped slots.

VueJS 2: Catch event of direct child component

I'm currently trying to get a simple Tabs/Tab component up and running.
It seems like something in the event handling mechanism has changed, therefore I can't get it to work.
Current implementation:
Tabs.vue
<template>
<div class="tabbed-pane">
<ul class="tab-list">
<li class="tab" v-for="tab in tabs" #click="activateTab(tab)">{{ tab.header }}</li>
</ul>
<slot></slot>
</div>
</template>
<script>
import hub from '../eventhub';
export default {
props: [],
data() {
return {
tabs: []
}
},
created() {
this.$on('tabcreated', this.registerTab)
},
methods: {
registerTab(tab) {
this.tabs.push(tab);
},
activateTab(tab) {
}
}
}
</script>
Tab.vue
<template>
<div class="tab-pane" v-show="active">
<slot></slot>
</div>
</template>
<script>
import hub from '../eventhub';
export default {
props: {
'header': String
},
data() {
return {
active: false
}
},
mounted() {
this.$emit('tabcreated', this);
}
}
</script>
eventhub.js
import Vue from 'vue';
export default new Vue();
View
<tabs>
<tab header="Test">
First Tab
</tab>
<tab header="Test2">
Second Tab
</tab>
<tab header="Test3">
Third Tab
</tab>
</tabs>
I've tried the following things:
use a Timeout for the $emit to test if it's a timing issue (it is
not)
use #tabcreated in the root element of the Tabs components
template
It works if...
... I use the suggested "eventhub" feature (replacing this.$on and
this.$emit with hub.$on and hub.$emit)
but this is not suitable for me, as I want to use the Tabs component multiple times on the same page, and doing it with the "eventhub" feature wouldn't allow that.
... I use this.$parent.$emit
but this just feels weird and wrong.
The documentation states that it IS possible to listen for events triggered by $emit on direct child components
https://v2.vuejs.org/v2/guide/migration.html#dispatch-and-broadcast-replaced
Does anyone have an Idea?
You're right, in vue 2, there is no more $dispatch. $emit could work for a single component but it will be scoped to himself (this). The recommended solution is to use a global event manager, the eventhub.
the eventhub can be stored in the window object to be used anywhere without import, I like to declare in my main.js file like this:
window.bus = new Vue()
and then in whatever component:
bus.$emit(...)
bus.$on(...)
It works just the same as this.$root.$emit / this.$root.$on. You said it works when you call this.$parent.$emit, but this code, simulate a scoped emit in the parent component but fired from the child, not good.
What I understand in your code is that you want to have an array of created tabs, but to do what with them ?
Instead of storing the tab instance in the parent and then activate from the parent, you should think about a more functional way.
The activateTab method should be declared on the tab component and manage the instanciation through the data, something like:
Tabs.vue
<template>
<div class="tabbed-pane">
<ul class="tab-list">
<tab v-for="tab in tabs" :header="tab.header"></tab>
</ul>
</div>
</template>
<script>
import hub from '../eventhub';
import Tab from 'path/to/Tab.vue';
export default {
components: [Tab],
props: [],
data() {
return {
tabs: ['First Tab', 'Second Tab', 'Third Tab']
}
}
}
</script>
Tab.vue
<template>
<div class="tab tab-pane" #click:activeTab()>
<span v-show="active">Activated</span>
<span>{{ header }}</span>
</div>
</template>
<script>
import hub from '../eventhub';
export default {
props: {
'header': String
},
data() {
return {
active: false
}
},
methods: {
activeTab () {
this.active = true
}
}
}
</script>
This way, your Tab is more independant. For parent/child communication keep this in mind :
parent to child > via props
child to parent > via $emit (global bus)
If you need a more complexe state management you definitely should take a look at vuex.
Edit
Tabs.vue
<template>
<div class="tabbed-pane">
<ul class="tab-list">
<tab v-for="tabData in tabs" :custom="tabData"></tab>
</ul>
</div>
</template>
<script>
import Tab from 'path/to/Tab.vue';
export default {
components: [Tab],
props: [],
data() {
return {
tabs: [
{foo: "foo 1"},
{foo: "foo 2"}
{foo: "foo 3"}
]
}
}
}
</script>
Tab.vue
<template>
<div class="tab tab-pane" #click:activeTab()>
<span v-show="active">Activated</span>
<span>{{ custom.foo }}</span>
</div>
</template>
<script>
export default {
props: ['custom'],
data() {
return {
active: false
}
},
methods: {
activeTab () {
this.active = true
}
}
}
</script>
This is what I don't like about VueJS (2), there is no convenient way of catching events emitted from child components to the parent component.
Anyways an alternative to this is if you do not want to use the eventhub approach, specially if you are only going to have an event communication between related components ( child and parent ) and not with non-related components, then you can do these steps.
reference your parent vue component on its data property (very important, you can't just pass this to the child component)
pass that parent vue component reference as an attribute to the child component ( make sure to bind it)
trigger the appropriate event of the parent component inside the child component whenever a desired event is emitted
Pseudo code
// Parent vue component
Vue.component( 'parent_component' , {
// various codes here ...
data : {
parent_component_ref : this // reference to the parent component
},
methods : {
custom_event_cb : function() {
// custom method to execute when child component emits 'custom_event'
}
}
// various codes here ...
} );
// Parent component template
<div id="parent_component">
<child_component :parent_component_ref="parent_component_ref"></child_component>
</div>
// Child component
Vue.component( 'child_component' , {
// various codes here ...
props : [ 'parent_component_ref' ],
mounted : function() {
this.$on( 'custom_event' , this.parent_component_ref.custom_event_cb );
this.$emit( 'custom_event' );
},
// You can also, of course, emit the event on events inside the child component, ex. button click, etc..
} );
Hope this helps anyone.
Use v-on="$listeners", which is available since Vue v2.4.0. You can then subscribe to any event you want on the parent, see fiddle.
Credit to BogdanL from Vue Support # Discord.

Categories